diff --git a/Cargo.lock b/Cargo.lock index 624f3cded..4d1b7ff14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,9 +47,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "annotate-snippets" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92570a3f9c98e7e84df84b71d0965ac99b1871fcd75a3773a3bd1bad13f64cf7" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" dependencies = [ "anstyle", "memchr", @@ -97,15 +97,15 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -130,9 +130,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" dependencies = [ "serde", ] @@ -193,7 +193,7 @@ dependencies = [ "serde", "serde_derive", "unicode-ident", - "winnow 1.0.1", + "winnow 1.0.3", ] [[package]] @@ -215,9 +215,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -253,15 +253,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", @@ -282,9 +282,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" dependencies = [ "axum", "axum-core", @@ -421,6 +421,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -462,15 +468,15 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn", ] [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "blake2" @@ -510,9 +516,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -521,25 +527,34 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] -name = "built" -version = "0.8.0" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -561,15 +576,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bytesize" -version = "2.3.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" [[package]] name = "bzip2-sys" @@ -583,18 +598,18 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" dependencies = [ "serde_core", ] [[package]] name = "cargo-platform" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", "serde_core", @@ -627,22 +642,16 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -666,13 +675,13 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -686,11 +695,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ + "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", + "windows-link", ] [[package]] @@ -706,9 +720,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -726,9 +740,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -784,9 +798,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -798,9 +812,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -835,7 +849,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "parking_lot", - "reqwest 0.13.2", + "reqwest 0.13.4", "rustls", "sentry", "sentry-tower", @@ -893,7 +907,7 @@ dependencies = [ "ctor", "dtor", "futures", - "hmac", + "hmac 0.13.0", "http", "http-body-util", "hyper", @@ -901,8 +915,8 @@ dependencies = [ "itertools 0.15.0", "lettre", "log", - "rand 0.10.1", - "reqwest 0.13.2", + "rand 0.10.2", + "reqwest 0.13.4", "ruma", "ruminuwuity", "serde", @@ -958,13 +972,14 @@ dependencies = [ "lock_api", "log", "maplit", - "nix 0.31.2", + "nix", "num-traits", + "openidconnect", "parking_lot", "rand 0.10.2", "rand_core 0.6.4", "regex", - "reqwest 0.13.2", + "reqwest 0.13.4", "resolvematrix", "ruma", "sanitize-filename", @@ -1076,7 +1091,7 @@ dependencies = [ "either", "futures", "governor", - "hickory-resolver 0.26.1", + "hickory-resolver", "http", "image", "ipaddress", @@ -1086,11 +1101,12 @@ dependencies = [ "loole", "lru-cache", "nonzero_ext", + "openidconnect", "rand 0.10.2", "recaptcha-verify", "regex", "reqwest 0.12.28", - "reqwest 0.13.2", + "reqwest 0.13.4", "resolvematrix", "ruma", "ruminuwuity", @@ -1128,7 +1144,8 @@ dependencies = [ "futures", "lettre", "memory-serve", - "rand 0.10.1", + "openidconnect", + "rand 0.10.2", "recaptcha-verify", "reqwest 0.12.28", "ruma", @@ -1415,10 +1432,22 @@ dependencies = [ ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -1491,8 +1520,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1509,22 +1548,46 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "date_header" @@ -1549,6 +1612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1558,7 +1622,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1591,7 +1654,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "crypto-common 0.1.7", + "const-oid", + "crypto-common 0.1.6", "subtle", ] @@ -1618,9 +1682,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1651,6 +1715,26 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -1677,13 +1761,34 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" version = "0.4.1" @@ -1718,18 +1823,6 @@ dependencies = [ "encoding_rs", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1780,6 +1873,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1840,12 +1943,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1863,9 +1960,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" dependencies = [ "autocfg", "tokio", @@ -1960,9 +2057,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -1983,12 +2080,13 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2020,16 +2118,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", - "wasip2", - "wasip3", + "rand_core 0.10.1", ] [[package]] @@ -2084,10 +2180,21 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.4.13" +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -2095,7 +2202,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2138,12 +2245,9 @@ checksum = "647deb1583b14d160f85f3ff626f20b6edd366e3852c9843b06077388f794cb6" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" @@ -2153,14 +2257,14 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hdrhistogram" @@ -2229,36 +2333,11 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "hickory-proto 0.26.1", + "hickory-proto", "idna", "ipnet", - "jni 0.22.4", - "rand 0.10.1", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.3", - "ring", + "jni", + "rand 0.10.2", "thiserror 2.0.18", "tinyvec", "tokio", @@ -2275,10 +2354,10 @@ dependencies = [ "data-encoding", "idna", "ipnet", - "jni 0.22.4", + "jni", "once_cell", "prefix-trie", - "rand 0.10.1", + "rand 0.10.2", "ring", "serde", "thiserror 2.0.18", @@ -2287,27 +2366,6 @@ dependencies = [ "url", ] -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto 0.25.2", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.3", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "hickory-resolver" version = "0.26.1" @@ -2317,15 +2375,15 @@ dependencies = [ "cfg-if", "futures-util", "hickory-net", - "hickory-proto 0.26.1", + "hickory-proto", "ipconfig", "ipnet", - "jni 0.22.4", + "jni", "moka", "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "resolv-conf", "serde", "smallvec", @@ -2335,6 +2393,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac" version = "0.13.0" @@ -2371,9 +2447,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2425,24 +2501,24 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -2462,19 +2538,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -2512,6 +2588,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -2594,12 +2694,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2619,9 +2713,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2655,6 +2749,17 @@ dependencies = [ "quick-error", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -2662,7 +2767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2710,13 +2815,12 @@ dependencies = [ ] [[package]] -name = "iri-string" -version = "0.7.12" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "memchr", - "serde", + "either", ] [[package]] @@ -2752,22 +2856,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -2777,7 +2865,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", "thiserror 2.0.18", @@ -2798,15 +2886,6 @@ dependencies = [ "syn", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -2838,13 +2917,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2904,18 +2982,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +dependencies = [ + "spin", +] [[package]] name = "lettre" -version = "0.11.21" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabda5859ee7c06b995b9d1165aa52c39110e079ef609db97178d86aeb051fa7" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "async-trait", "base64 0.22.1", @@ -2943,9 +3018,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -2968,10 +3043,16 @@ dependencies = [ ] [[package]] -name = "libz-sys" -version = "1.1.28" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -3025,9 +3106,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "loole" @@ -3119,9 +3200,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memory-serve" @@ -3157,9 +3238,9 @@ dependencies = [ [[package]] name = "minicbor" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70eae6d4f18f7d76877fe7b13f0bc21f7c2b7239d2041c338335f7b388d0dd7" +checksum = "1b7a5041e12946f8b7d3f5a9d96383a19d694b9335457c522be7815b9abafb02" [[package]] name = "minicbor-serde" @@ -3198,9 +3279,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -3249,21 +3330,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.30.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nix" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -3327,14 +3396,30 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" dependencies = [ "num-integer", "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -3346,9 +3431,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -3388,6 +3473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3400,6 +3486,26 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + [[package]] name = "objc2" version = "0.6.4" @@ -3578,6 +3684,37 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac 0.12.1", + "http", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.6", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2 0.10.9", + "subtle", + "thiserror 1.0.69", + "url", +] + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3608,7 +3745,7 @@ dependencies = [ "bytes", "http", "opentelemetry", - "reqwest 0.13.2", + "reqwest 0.13.4", ] [[package]] @@ -3623,7 +3760,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest 0.13.2", + "reqwest 0.13.4", "thiserror 2.0.18", "tokio", "tonic", @@ -3655,21 +3792,30 @@ dependencies = [ "opentelemetry", "percent-encoding", "portable-atomic", - "rand 0.9.3", + "rand 0.9.4", "thiserror 2.0.18", "tokio", "tokio-stream", ] [[package]] -name = "os_info" -version = "3.14.0" +name = "ordered-float" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" dependencies = [ "android_system_properties", "log", - "nix 0.30.1", + "nix", "objc2", "objc2-foundation", "objc2-ui-kit", @@ -3677,6 +3823,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -3748,6 +3918,15 @@ dependencies = [ "syn", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3761,7 +3940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -3790,7 +3969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -3804,18 +3983,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -3828,6 +4007,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3840,9 +4030,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "png" @@ -3905,13 +4095,12 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "primeorder" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "proc-macro2", - "syn", + "elliptic-curve", ] [[package]] @@ -3920,7 +4109,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -3969,9 +4158,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -3979,9 +4168,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -3992,18 +4181,18 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] [[package]] name = "pulldown-cmark" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ "bitflags", "memchr", @@ -4019,9 +4208,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "quick-error" @@ -4031,9 +4220,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -4052,15 +4241,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.3", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -4088,9 +4277,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -4115,20 +4304,22 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -4139,8 +4330,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.0", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -4173,9 +4374,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "recaptcha-verify" @@ -4198,10 +4399,30 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.12.3" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -4222,9 +4443,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -4245,6 +4466,7 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", + "quinn", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -4255,19 +4477,20 @@ dependencies = [ "tokio", "tokio-rustls", "tower", - "tower-http 0.6.8", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -4277,7 +4500,7 @@ dependencies = [ "h2", "h3", "h3-quinn", - "hickory-resolver 0.25.2", + "hickory-resolver", "http", "http-body", "http-body-util", @@ -4300,7 +4523,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower", - "tower-http 0.6.8", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", @@ -4322,16 +4545,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "318f1067d605ee34de83fabc0f0b73e8664b580abea8d5e45742fab534e2b18a" dependencies = [ "futures", - "hickory-resolver 0.26.1", + "hickory-resolver", "num-traits", "parking_lot", - "reqwest 0.13.2", + "reqwest 0.13.4", "serde", "serde_json", "thiserror 2.0.18", "tracing", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -4346,6 +4579,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "ruma" version = "0.15.1" @@ -4409,9 +4662,9 @@ dependencies = [ "bytes", "date_header", "form_urlencoded", - "getrandom 0.4.2", + "getrandom 0.4.3", "http", - "indexmap", + "indexmap 2.14.0", "js_int", "konst", "percent-encoding", @@ -4438,7 +4691,7 @@ version = "0.33.0" source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" dependencies = [ "as_variant", - "indexmap", + "indexmap 2.14.0", "js_int", "js_option", "pulldown-cmark", @@ -4522,7 +4775,7 @@ dependencies = [ "ed25519-dalek", "memchr", "pkcs8", - "rand 0.10.1", + "rand 0.10.2", "ruma-common", "serde_json", "sha2 0.10.9", @@ -4590,9 +4843,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -4618,9 +4871,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", @@ -4634,9 +4887,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -4646,9 +4899,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -4656,13 +4909,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -4683,9 +4936,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.11" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -4746,6 +4999,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -4761,6 +5038,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -4802,7 +5093,7 @@ checksum = "1975064d9f3d9d87a7c8f0371f7fac10d876906ca3e39faad72e61c263ff9b87" dependencies = [ "cfg_aliases", "httpdate", - "reqwest 0.13.2", + "reqwest 0.13.4", "sentry-backtrace", "sentry-contexts", "sentry-core", @@ -4845,7 +5136,7 @@ version = "0.48.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cff96247f4dc36867511d108709e703eb354143e7893319d763d2dd1edb4f48" dependencies = [ - "rand 0.9.3", + "rand 0.9.4", "sentry-types", "serde", "serde_json", @@ -4918,7 +5209,7 @@ checksum = "8d7e78132ccfb4a1c777b959fb2944d1f6d5145bffe6be2a98ee6b25d244f72c" dependencies = [ "debugid", "hex", - "rand 0.9.3", + "rand 0.9.4", "serde", "serde_json", "thiserror 2.0.18", @@ -4956,6 +5247,16 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4978,12 +5279,12 @@ dependencies = [ [[package]] name = "serde_html_form" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f" +checksum = "8f0346d7a342ab90f405cfc08f25d15075f944f42fcabbc5eac923829fa6d228" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.14.0", "itoa", "serde_core", "zmij", @@ -5014,10 +5315,19 @@ dependencies = [ ] [[package]] -name = "serde_regex" -version = "1.1.0" +name = "serde_plain" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafc8d0c5330cecff10f16b459b479fd9acaa5b4acd7167301414e21b0057012" dependencies = [ "regex", "serde", @@ -5053,6 +5363,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5125,6 +5467,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook" version = "0.3.18" @@ -5162,6 +5510,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -5189,9 +5538,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -5211,23 +5560,29 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spinning_top" version = "0.3.0" @@ -5298,9 +5653,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -5470,12 +5825,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -5485,15 +5839,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -5526,9 +5880,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5616,13 +5970,13 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.1", + "winnow 1.0.3", ] [[package]] @@ -5649,7 +6003,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -5659,14 +6013,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.1", + "winnow 1.0.3", ] [[package]] @@ -5675,7 +6029,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.3", ] [[package]] @@ -5692,9 +6046,9 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", @@ -5721,9 +6075,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -5732,9 +6086,9 @@ dependencies = [ [[package]] name = "tonic-types" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a875a902255423d34c1f20838ab374126db8eb41625b7947a1d54113b0b7399" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" dependencies = [ "prost", "prost-types", @@ -5749,7 +6103,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5778,9 +6132,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "bitflags", @@ -5790,13 +6144,13 @@ dependencies = [ "http", "http-body", "http-body-util", - "iri-string", "pin-project-lite", "tokio", "tokio-util", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -5876,7 +6230,7 @@ dependencies = [ "futures", "http", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "serde", "serde_json", "thiserror 2.0.18", @@ -5999,9 +6353,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typewit" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19094686c694eb41b3b99dcc2f2975d4b078512fa22ae6c63f7ca318bdcff7" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" [[package]] name = "uname" @@ -6035,9 +6389,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -6051,12 +6405,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -6096,11 +6444,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -6128,7 +6476,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling", + "darling 0.20.11", "once_cell", "proc-macro-error2", "proc-macro2", @@ -6181,27 +6529,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -6212,9 +6551,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -6222,9 +6561,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6232,9 +6571,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -6245,35 +6584,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -6287,23 +6604,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -6333,9 +6638,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -6389,6 +6703,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -6424,15 +6773,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6460,21 +6800,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -6508,12 +6833,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6526,12 +6845,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6544,12 +6857,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6574,12 +6881,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6592,12 +6893,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6610,12 +6905,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6628,12 +6917,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6657,100 +6940,18 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" @@ -6788,9 +6989,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6811,18 +7012,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -6831,9 +7032,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -6852,9 +7053,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index f89a5e010..ce73384b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -403,6 +403,9 @@ default-features = false version = "0.11.0" default-features = false +[workspace.dependencies.openidconnect] +version = "4.0.1" + # optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring [workspace.dependencies.opentelemetry] version = "0.32.0" diff --git a/changelog.d/765.feature.md b/changelog.d/765.feature.md new file mode 100644 index 000000000..906c316c0 --- /dev/null +++ b/changelog.d/765.feature.md @@ -0,0 +1 @@ +Added support for linking an external identity provider with OIDC. Contributed by @ginger. diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 28d1022cf..a98a35639 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -2028,3 +2028,96 @@ # legacy authentication will be unable to log in. # #compatibility_mode = "hybrid" + +#[global.oauth.oidc] +# Uncommenting this section will enable Continuwuity's support for +# authenticating users using an OpenID Connect-compatible identity provider. +# This is referred to as "delegated authentication". +# +# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if +# the `global.oauth.compatibility_mode` setting is set to `exclusive`. +# Matrix clients which do not support OAuth login (also referred to as "next-gen auth") will NOT be able +# to log in while delegated authentication is active. + + +# The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to +# automatically fetch the identity provider's metadata from this URL. +# Generally you should set this to the base domain your identity provider +# runs on. +# +#discovery_url = + +# The OAuth client ID for Continuwuity to use when communicating with the +# identity provider. +# +#client_id = + +# The OAuth client secret for Continuwuity to use when communicating with +# the identity provider. +# +#client_secret = + +# A path to a file which Continuwuity will read the client secret from. +# If this option is set, it will override `client_secret`. +# +# The server will fail to start if the file cannot be read. +# +#client_secret_file = + +# Additional scopes Continuwuity should request from the IDP. This may be +# necessary to access certain claims. Continuwuity always requests the +# `openid` scope. +# +#additional_scopes = [] + +# Whether the user should be prompted to choose a localpart +# when signing in for the first time. If this is `false`, Continuwuity +# will attempt to use the value of the `preferred_username_claim` +# (see below) as the user's localpart. Authentication will +# fail if this claim is missing or is not a valid localpart. +# +#prompt_for_localpart = true + +# The claim to use for the user's localpart, if `prompt_for_localpart` is +# false. +# +#preferred_username_claim = "preferred_username" + +# The claim which will be used to set the user's email address, +# either on initial registration or on every login depending on +# the value of `profile_key_import_mode`. Continuwuity assumes that +# the IDP has taken care of verifying that the user controls the email +# address it provides. +# +# This option does nothing if SMTP is not configured. +# +# If this option is set, and `profile_key_import_mode` is `on_login`, +# users will not be able to change their email addresses themselves. +# +#email_claim = "email" + +# Defines how claims returned from the IDP should be mapped to a user's +# profile data. The profile field named in each key will be set from the +# claim named in the corresponding value when the user first registers, +# and possibly on subsequent logins as well, depending on the value of +# `profile_key_import_mode` (see below). +# +# Per-room overrides to the user's display name or avatar will be +# preserved by the import process. +# +# SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will +# perform a HTTP GET to the URL in the mapped claim and use the returned +# file as the user's profile picture. Make sure your users are not able +# to set the value of the mapped claim to an arbitrary URL. +# +#profile_key_map = { displayname = "name" } + +# When profile keys should be imported from the IDP's claims. +# +# - "on_registration": Listed keys will be imported once, when the user +# logs in for the first time and their shadow account is created. +# - "on_login": Listed keys will be imported every time the user logs in. +# Additionally, users will not be able to manually edit any listed keys +# through their Matrix client. +# +#profile_key_import_mode = "on_registration" diff --git a/docs/reference/admin/debug.md b/docs/reference/admin/debug.md index 9ad88ef3e..ea0d83bc2 100644 --- a/docs/reference/admin/debug.md +++ b/docs/reference/admin/debug.md @@ -10,7 +10,13 @@ Echo input of admin command ## `!admin debug get-auth-chain` -Get the auth_chain of a PDU +Loads the auth_chain of a PDU, reporting how long it took + +## `!admin debug show-auth-chain` + +Walks & displays the auth_chain of a PDU in a mermaid graph format. + +This is useless to basically anyone but developers, and is also probably slow and memory hungry. ## `!admin debug parse-pdu` @@ -44,6 +50,12 @@ This is functionally equivalent to `GET /_matrix/client/v3/rooms/{roomid}/state` Of course the check is still done on the actual client API. +## `!admin debug get-state-at` + +Gets all the room state events at the specified event. + +State at event might not be available for some PDUs, such as rejected ones. + ## `!admin debug get-signing-keys` Get and display signing keys from local cache or remote server diff --git a/docs/reference/admin/index.md b/docs/reference/admin/index.md index 741aa78ba..3dbdf8105 100644 --- a/docs/reference/admin/index.md +++ b/docs/reference/admin/index.md @@ -14,6 +14,7 @@ Admin commands allow server administrators to manage the server from within thei - [`!admin appservices`](appservices/): Commands for managing appservices - [`!admin users`](users/): Commands for managing local users - [`!admin token`](token/): Commands for managing registration tokens +- [`!admin oidc`](oidc/): Commands for managing OIDC - [`!admin rooms`](rooms/): Commands for managing rooms - [`!admin federation`](federation/): Commands for managing federation - [`!admin server`](server/): Commands for managing the server diff --git a/docs/reference/admin/oidc.md b/docs/reference/admin/oidc.md new file mode 100644 index 000000000..4a0a6e1fc --- /dev/null +++ b/docs/reference/admin/oidc.md @@ -0,0 +1,13 @@ + +# `!admin oidc` + +Commands for managing OIDC + + +## `!admin oidc link` + +Link a user ID to the given subject claim + +## `!admin oidc unlink` + +Unlink the given subject claim from its associated user ID diff --git a/docs/reference/admin/users.md b/docs/reference/admin/users.md index 7058f5993..f2403251f 100644 --- a/docs/reference/admin/users.md +++ b/docs/reference/admin/users.md @@ -12,10 +12,6 @@ Create a new user Reset user password -## `!admin users issue-password-reset-link` - -Issue a self-service password reset link for a user - ## `!admin users get-email` Get a user's associated email address @@ -96,6 +92,14 @@ Disables login for the specified user without deactivating or locking their acco List local users in the database +## `!admin users list-invited-rooms` + +Lists all the rooms (local and remote) that the specified user is invited to + +## `!admin users reject-all-invites` + +Manually make a user reject all current invites + ## `!admin users list-joined-rooms` Lists all the rooms (local and remote) that the specified user is joined in diff --git a/package-lock.json b/package-lock.json index d5aef7c33..53da3a3e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -383,9 +383,9 @@ } }, "node_modules/@rspress/core": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.15.tgz", - "integrity": "sha512-epLmUXYscNRw/GtQZx2oknoBE9wKbCrUGEOrQEDI4Qq8X32GdM4d7itzuHsliY7q3IbffKx8rMVbvlmygEocTQ==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.16.tgz", + "integrity": "sha512-jJcYNNBKY/VLR8oxLqdd5uvm6bHahXeF3wSaoAe2U1hxWWwoP9k4rBTDx9X3JkUWcgnthu7UgtMiHeLs+2fhFg==", "dev": true, "license": "MIT", "dependencies": { @@ -393,7 +393,7 @@ "@mdx-js/react": "^3.1.1", "@rsbuild/core": "^2.0.15", "@rsbuild/plugin-react": "~2.0.1", - "@rspress/shared": "2.0.15", + "@rspress/shared": "2.0.16", "@shikijs/rehype": "^4.2.0", "@types/unist": "^3.0.3", "@unhead/react": "^2.1.15", @@ -412,11 +412,11 @@ "react-lazy-with-preload": "^2.2.1", "react-reconciler": "0.33.0", "react-render-to-markdown": "19.1.0", - "react-router-dom": "^7.15.1", + "react-router-dom": "^7.18.1", "rehype-external-links": "^3.0.0", "rehype-raw": "^7.0.0", "remark-cjk-friendly": "^2.3.1", - "remark-cjk-friendly-gfm-strikethrough": "^2.1.0", + "remark-cjk-friendly-gfm-strikethrough": "^2.3.1", "remark-gfm": "^4.0.1", "remark-mdx": "^3.1.1", "remark-parse": "^11.0.0", @@ -436,9 +436,9 @@ } }, "node_modules/@rspress/plugin-client-redirects": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.15.tgz", - "integrity": "sha512-bPf/KIHH7Y6huLTtK6JXwRfxM7zKjksoxm46+IBsF1wisw0doKkEKR9HwJydxWnykyKBbA2cuZOaoT4h174Z1w==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.16.tgz", + "integrity": "sha512-FEjZb+3lxpkEESdt0uWa4dRQU7d/Okn5SyX7CDaMHdHuqg4XdOidAQ/95ZzgXomN7YVPv40eVe/0is3oWnAjew==", "dev": true, "license": "MIT", "engines": { @@ -449,9 +449,9 @@ } }, "node_modules/@rspress/plugin-sitemap": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.15.tgz", - "integrity": "sha512-z1hbyGP79ZXdSGJxiWw7ZjmX8qW0q9nXMDxr14cVEg/wdj7ToVzGtZHw0wvTPE0YiKG3BMiGkVNfE1rdOaPXiQ==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.16.tgz", + "integrity": "sha512-zRPvKAGF8EexblJvrkhhHtD2Kqlbaw6XFjbIMM07gs0SIUKNg1o3T2I/uo5cvWtk0pGfYyyAJj94HoqLUgUsEw==", "dev": true, "license": "MIT", "engines": { @@ -462,9 +462,9 @@ } }, "node_modules/@rspress/shared": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.15.tgz", - "integrity": "sha512-o8aYwEzNuTmWnmKe91ntPv+34u3RbtAe+rcK9XC5MANOlgncwOaCs3bUa8/B1/llwyLoNgrpi+VB9bEiU11ZRQ==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.16.tgz", + "integrity": "sha512-FjBSfGtgrlR1bRJ0EQLyNo2qXUXxzb2QE3NPRIICf8TKpP413gRCMyMRtzhbIqD4Gn7k+em82VAkWTAAQjQLTw==", "dev": true, "license": "MIT", "dependencies": { @@ -2869,9 +2869,9 @@ } }, "node_modules/react-router": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", - "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "dev": true, "license": "MIT", "dependencies": { @@ -2892,13 +2892,13 @@ } }, "node_modules/react-router-dom": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", - "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", "dev": true, "license": "MIT", "dependencies": { - "react-router": "7.17.0" + "react-router": "7.18.1" }, "engines": { "node": ">=20.0.0" @@ -3081,9 +3081,9 @@ } }, "node_modules/remark-cjk-friendly-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-3Kyq2hjY7V7eU8MbVbWW6QQLN81pjJcIvKHvPxr8hZZmcq/9wqm3MJ3iUG34Ch9QTM4WHN+a1JVAVC1fSi5mig==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.3.1.tgz", + "integrity": "sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/admin/admin.rs b/src/admin/admin.rs index 2bebc7523..bb9574a5c 100644 --- a/src/admin/admin.rs +++ b/src/admin/admin.rs @@ -1,5 +1,5 @@ use clap::Parser; -use conduwuit::Result; +use conduwuit::{Err, Result}; use crate::{ appservice::{self, AppserviceCommand}, @@ -8,6 +8,7 @@ use crate::{ debug::{self, DebugCommand}, federation::{self, FederationCommand}, media::{self, MediaCommand}, + oidc::{self, OidcCommand}, query::{self, QueryCommand}, room::{self, RoomCommand}, server::{self, ServerCommand}, @@ -18,44 +19,48 @@ use crate::{ #[derive(Debug, Parser)] #[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())] pub enum AdminCommand { - #[command(subcommand)] /// Commands for managing appservices + #[command(subcommand)] Appservices(AppserviceCommand), - #[command(subcommand)] /// Commands for managing local users + #[command(subcommand)] Users(UserCommand), - #[command(subcommand)] /// Commands for managing registration tokens + #[command(subcommand)] Token(TokenCommand), + /// Commands for managing OIDC #[command(subcommand)] + Oidc(OidcCommand), + /// Commands for managing rooms + #[command(subcommand)] Rooms(RoomCommand), - #[command(subcommand)] /// Commands for managing federation + #[command(subcommand)] Federation(FederationCommand), - #[command(subcommand)] /// Commands for managing the server + #[command(subcommand)] Server(ServerCommand), - #[command(subcommand)] /// Commands for managing media + #[command(subcommand)] Media(MediaCommand), - #[command(subcommand)] /// Commands for checking integrity + #[command(subcommand)] Check(CheckCommand), - #[command(subcommand)] /// Commands for debugging things + #[command(subcommand)] Debug(DebugCommand), - #[command(subcommand)] /// Low-level queries for database getters and iterators + #[command(subcommand)] Query(QueryCommand), } @@ -80,6 +85,16 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res context.bail_restricted()?; token::process(command, context).await }, + | Oidc(command) => { + // OIDC commands are all restricted + context.bail_restricted()?; + + if !context.services.oidc.enabled() { + return Err!("OIDC is not configured"); + } + + oidc::process(command, context).await + }, | Rooms(command) => room::process(command, context).await, | Federation(command) => federation::process(command, context).await, | Server(command) => server::process(command, context).await, diff --git a/src/admin/check/commands.rs b/src/admin/check/commands.rs index 14ef4f78f..db17bb109 100644 --- a/src/admin/check/commands.rs +++ b/src/admin/check/commands.rs @@ -6,7 +6,12 @@ use crate::Context; impl Context<'_> { pub(super) async fn check_all_users(&self) -> Result { let timer = tokio::time::Instant::now(); - let users = self.services.users.stream().collect::>().await; + let users = self + .services + .users + .stream_local_users() + .collect::>() + .await; let query_time = timer.elapsed(); let total = users.len(); diff --git a/src/admin/debug/commands.rs b/src/admin/debug/commands.rs index 8430a4d02..d59ba9270 100644 --- a/src/admin/debug/commands.rs +++ b/src/admin/debug/commands.rs @@ -616,7 +616,7 @@ impl crate::Context<'_> { // Force E2EE device list updates for all users self.services .users - .stream() + .stream_local_users() .for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await) .await; diff --git a/src/admin/mod.rs b/src/admin/mod.rs index 1bc33691c..fd73c3f15 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod check; pub(crate) mod debug; pub(crate) mod federation; pub(crate) mod media; +pub(crate) mod oidc; pub(crate) mod query; pub(crate) mod room; pub(crate) mod server; diff --git a/src/admin/oidc/commands.rs b/src/admin/oidc/commands.rs new file mode 100644 index 000000000..439d40e2c --- /dev/null +++ b/src/admin/oidc/commands.rs @@ -0,0 +1,25 @@ +use conduwuit::Result; + +use crate::utils::parse_active_local_user_id; + +impl crate::Context<'_> { + pub(super) async fn oidc_link(&self, user_id: String, subject: String) -> Result { + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + + self.services.oidc.link_user(&user_id, &subject); + + self.write_str(&format!("Subject `{subject}` linked to account `{user_id}`.")) + .await?; + + Ok(()) + } + + pub(super) async fn oidc_unlink(&self, subject: String) -> Result { + self.services.oidc.unlink_user(&subject); + + self.write_str(&format!("Subject `{subject}` unlinked.")) + .await?; + + Ok(()) + } +} diff --git a/src/admin/oidc/mod.rs b/src/admin/oidc/mod.rs new file mode 100644 index 000000000..591563919 --- /dev/null +++ b/src/admin/oidc/mod.rs @@ -0,0 +1,22 @@ +mod commands; + +use clap::Subcommand; +use conduwuit::Result; +use conduwuit_macros::admin_command_dispatch; + +#[admin_command_dispatch] +#[derive(Debug, Subcommand)] +pub enum OidcCommand { + /// Link a user ID to the given subject claim. + #[clap(name = "link")] + OidcLink { + user_id: String, + subject: String, + }, + + /// Unlink the given subject claim from its associated user ID. + #[clap(name = "unlink")] + OidcUnlink { + subject: String, + }, +} diff --git a/src/admin/query/users.rs b/src/admin/query/users.rs index 13fe9a888..7449f0689 100644 --- a/src/admin/query/users.rs +++ b/src/admin/query/users.rs @@ -191,8 +191,13 @@ impl crate::Context<'_> { async fn iter_users(&self) -> Result { let timer = tokio::time::Instant::now(); - let result: Vec = - self.services.users.stream().map(Into::into).collect().await; + let result: Vec = self + .services + .users + .stream_local_users() + .map(Into::into) + .collect() + .await; let query_time = timer.elapsed(); @@ -202,7 +207,7 @@ impl crate::Context<'_> { async fn iter_users2(&self) -> Result { let timer = tokio::time::Instant::now(); - let result: Vec<_> = self.services.users.stream().collect().await; + let result: Vec<_> = self.services.users.stream_local_users().collect().await; let result: Vec<_> = result .into_iter() .map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned()) diff --git a/src/admin/token/commands.rs b/src/admin/token/commands.rs index 0e5ae6132..21882bc32 100644 --- a/src/admin/token/commands.rs +++ b/src/admin/token/commands.rs @@ -44,7 +44,7 @@ impl crate::Context<'_> { .services .config .oauth - .compatibility_mode + .compatibility_mode() .oauth_available() { self.write_str(&format!( diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 26dadc5b5..a76e3fe92 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -20,7 +20,7 @@ use ruma::{ tag::{TagEvent, TagEventContent, TagInfo}, }, }; -use service::users::HashedPassword; +use service::users::{AccountStatus, HashedPassword}; use crate::{ get_room_info, @@ -59,8 +59,8 @@ impl crate::Context<'_> { self.services .users - .create_local_account(&user_id, HashedPassword::new(password)?, None) - .await; + .create_local_account(&user_id, Some(HashedPassword::new(password)?), None) + .await?; self.write_str(&format!("Created user {user_id} with password `{password}`")) .await @@ -103,15 +103,12 @@ impl crate::Context<'_> { pub(super) async fn suspend(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; + let user_id = parse_active_local_user_id(self.services, &user_id).await?; if user_id == self.services.globals.server_user { return Err!("Not allowed to suspend the server service account.",); } - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } if self.services.users.is_admin(&user_id).await { return Err!("Admin users cannot be suspended."); } @@ -127,15 +124,12 @@ impl crate::Context<'_> { pub(super) async fn unsuspend(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; + let user_id = parse_active_local_user_id(self.services, &user_id).await?; if user_id == self.services.globals.server_user { return Err!("Not allowed to unsuspend the server service account.",); } - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } self.services.users.unsuspend_account(&user_id).await; self.write_str(&format!("User {user_id} has been unsuspended.")) @@ -147,6 +141,7 @@ impl crate::Context<'_> { logout: bool, username: String, password: Option, + convert_to_local_account: bool, ) -> Result { let user_id = parse_local_user_id(self.services, &username)?; @@ -159,15 +154,37 @@ impl crate::Context<'_> { let new_password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); + let new_password_hash = HashedPassword::new(&new_password)?; - self.services - .users - .set_password(&user_id, Some(HashedPassword::new(&new_password)?)); + if convert_to_local_account { + self.services + .users + .convert_to_local_account(&user_id, new_password_hash) + .await?; + } else { + match self.services.users.status(&user_id).await { + | AccountStatus::Active if !self.services.users.is_shadow(&user_id).await => { + self.services + .users + .set_password(&user_id, new_password_hash) + .await?; + }, + | AccountStatus::NotFound => { + return Err!("The provided user does not exist."); + }, + | _ => { + return Err!( + "The provided user is a shadow or deactivated account. To convert it to \ + a local account, pass the --convert-to-local-account flag." + ); + }, + } - self.write_str(&format!( - "Successfully reset the password for user {user_id}: `{new_password}`" - )) - .await?; + self.write_str(&format!( + "Successfully reset the password for user {user_id}: `{new_password}`" + )) + .await?; + } if logout { self.services @@ -919,21 +936,16 @@ impl crate::Context<'_> { pub(super) async fn lock(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + if user_id == self.services.globals.server_user { return Err!("Not allowed to lock the server service account.",); } - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } if self.services.users.is_admin(&user_id).await { return Err!("Admin users cannot be locked."); } + self.services .users .lock_account(&user_id, self.sender_or_service_user()) @@ -945,11 +957,8 @@ impl crate::Context<'_> { pub(super) async fn unlock(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + self.services.users.unlock_account(&user_id).await; self.write_str(&format!("User {user_id} has been unlocked.")) @@ -958,21 +967,16 @@ impl crate::Context<'_> { pub(super) async fn logout(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + if user_id == self.services.globals.server_user { return Err!("Not allowed to log out the server service account.",); } - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } if self.services.users.is_admin(&user_id).await { return Err!("You cannot forcefully log out admin users."); } + self.services .users .all_device_ids(&user_id) @@ -989,18 +993,12 @@ impl crate::Context<'_> { pub(super) async fn disable_login(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + if user_id == self.services.globals.server_user { return Err!("Not allowed to disable login for the server service account.",); } - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } if self.services.users.is_admin(&user_id).await { return Err!("Admin users cannot have their login disallowed."); } @@ -1014,14 +1012,8 @@ impl crate::Context<'_> { pub(super) async fn enable_login(&self, user_id: String) -> Result { self.bail_restricted()?; - let user_id = parse_local_user_id(self.services, &user_id)?; - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - if !self.services.users.exists(&user_id).await { - return Err!("User {user_id} does not exist."); - } + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + self.services.users.enable_login(&user_id); self.write_str(&format!("{user_id} can now log in.")).await @@ -1129,10 +1121,8 @@ impl crate::Context<'_> { } pub(super) async fn reset_push_rules(&self, user_id: String) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - if !self.services.users.is_active(&user_id).await { - return Err!("User is not active."); - } + let user_id = parse_active_local_user_id(self.services, &user_id).await?; + recreate_push_rules_and_return(self.services, &user_id).await?; self.write_str("Reset user's push rules to the server default.") .await diff --git a/src/admin/user/mod.rs b/src/admin/user/mod.rs index b2d05d650..c5278505a 100644 --- a/src/admin/user/mod.rs +++ b/src/admin/user/mod.rs @@ -27,6 +27,9 @@ pub enum UserCommand { username: String, /// New password for the user, if unspecified one is generated password: Option, + + #[arg(long)] + convert_to_local_account: bool, }, /// Get a user's associated email address. diff --git a/src/admin/utils.rs b/src/admin/utils.rs index 3df7d844a..f39985417 100644 --- a/src/admin/utils.rs +++ b/src/admin/utils.rs @@ -54,14 +54,7 @@ pub(crate) async fn parse_active_local_user_id( user_id: &str, ) -> Result { let user_id = parse_local_user_id(services, user_id)?; - - if !services.users.exists(&user_id).await { - return Err!("User {user_id:?} does not exist on this server."); - } - - if services.users.is_deactivated(&user_id).await? { - return Err!("User {user_id:?} is deactivated."); - } + services.users.status(&user_id).await.ensure_active()?; Ok(user_id) } diff --git a/src/api/client/account/mod.rs b/src/api/client/account/mod.rs index 807ff240a..686f24fa9 100644 --- a/src/api/client/account/mod.rs +++ b/src/api/client/account/mod.rs @@ -128,7 +128,8 @@ pub(crate) async fn change_password_route( services .users - .set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?)); + .set_password(&sender_user, HashedPassword::new(&body.new_password)?) + .await?; if body.logout_devices { // Logout all devices except the current one diff --git a/src/api/client/account/register.rs b/src/api/client/account/register.rs index 8f8ddf740..f59b05a7a 100644 --- a/src/api/client/account/register.rs +++ b/src/api/client/account/register.rs @@ -72,7 +72,7 @@ pub(crate) async fn register_route( .determine_registration_user_id(body.username.clone(), None, Some(appservice_info)) .await?; - services.users.create(&user_id, None)?; + services.users.create_shadow_account(&user_id).await?; user_id } else { @@ -97,8 +97,8 @@ pub(crate) async fn register_route( services .users - .create_local_account(&user_id, password, identity.email) - .await; + .create_local_account(&user_id, Some(password), identity.email) + .await?; user_id }; @@ -106,7 +106,7 @@ pub(crate) async fn register_route( let (token, device) = if !body.inhibit_login { // If UIAA is disabled, we can't create a device. In that case only appservices // can reach this point in the first place, so we return an error for them. - if !services.config.oauth.compatibility_mode.uiaa_available() { + if !services.config.oauth.compatibility_mode().uiaa_available() { return Err!(Request(AppserviceLoginUnsupported( "User-interactive appservice registration is not available on this server." ))); diff --git a/src/api/client/account/threepid.rs b/src/api/client/account/threepid.rs index dea02ea2c..3be31263b 100644 --- a/src/api/client/account/threepid.rs +++ b/src/api/client/account/threepid.rs @@ -25,6 +25,10 @@ pub(crate) async fn third_party_route( let sender_user = body.identity.expect_sender_user()?; let mut threepids = vec![]; + if !services.threepid.email_requirement().may_view() { + return Ok(get_3pids::v3::Response::new(vec![])); + } + if let Some(email) = services .threepid .get_email_for_localpart(sender_user.localpart()) diff --git a/src/api/client/admin/lock.rs b/src/api/client/admin/lock.rs index f748ef1ba..81b95dd61 100644 --- a/src/api/client/admin/lock.rs +++ b/src/api/client/admin/lock.rs @@ -12,20 +12,18 @@ pub(crate) async fn get_lock_status( State(services): State, body: Ruma, ) -> Result { - let (admin, active) = join( + let (admin, status) = join( services.users.is_admin(body.identity.expect_sender_user()?), - services.users.is_active(&body.user_id), + services.users.status(&body.user_id), ) .await; + if !admin { return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); } - if !services.globals.user_is_local(&body.user_id) { - return Err!(Request(InvalidParam("Can only check the lock status of local users"))); - } - if !active { - return Err!(Request(NotFound("Unknown user"))); - } + + status.ensure_active()?; + Ok(is_user_locked::v1::Response::new( services.users.is_locked(&body.user_id).await?, )) @@ -40,9 +38,9 @@ pub(crate) async fn put_lock_status( ) -> Result { let sender_user = body.identity.expect_sender_user()?; - let (sender_admin, active, target_admin) = join3( + let (sender_admin, status, target_admin) = join3( services.users.is_admin(sender_user), - services.users.is_active(&body.user_id), + services.users.status(&body.user_id), services.users.is_admin(&body.user_id), ) .await; @@ -50,18 +48,17 @@ pub(crate) async fn put_lock_status( if !sender_admin { return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); } - if !services.globals.user_is_local(&body.user_id) { - return Err!(Request(InvalidParam("Can only set the lock status of local users"))); - } - if !active { - return Err!(Request(NotFound("Unknown user"))); - } + + status.ensure_active()?; + if body.user_id == *sender_user { return Err!(Request(Forbidden("You cannot lock yourself"))); } + if target_admin { return Err!(Request(Forbidden("You cannot lock another server administrator"))); } + if services.users.is_locked(&body.user_id).await? == body.locked { // No change return Ok(lock_user::v1::Response::new(body.locked)); diff --git a/src/api/client/admin/suspend.rs b/src/api/client/admin/suspend.rs index cf1beca8d..fac32f12e 100644 --- a/src/api/client/admin/suspend.rs +++ b/src/api/client/admin/suspend.rs @@ -12,20 +12,18 @@ pub(crate) async fn get_suspended_status( State(services): State, body: Ruma, ) -> Result { - let (admin, active) = join( + let (admin, status) = join( services.users.is_admin(body.identity.expect_sender_user()?), - services.users.is_active(&body.user_id), + services.users.status(&body.user_id), ) .await; + if !admin { return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); } - if !services.globals.user_is_local(&body.user_id) { - return Err!(Request(InvalidParam("Can only check the suspended status of local users"))); - } - if !active { - return Err!(Request(NotFound("Unknown user"))); - } + + status.ensure_active()?; + Ok(is_user_suspended::v1::Response::new( services.users.is_suspended(&body.user_id).await?, )) @@ -40,9 +38,9 @@ pub(crate) async fn put_suspended_status( ) -> Result { let sender_user = body.identity.expect_sender_user()?; - let (sender_admin, active, target_admin) = join3( + let (sender_admin, status, target_admin) = join3( services.users.is_admin(sender_user), - services.users.is_active(&body.user_id), + services.users.status(&body.user_id), services.users.is_admin(&body.user_id), ) .await; @@ -50,18 +48,17 @@ pub(crate) async fn put_suspended_status( if !sender_admin { return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); } - if !services.globals.user_is_local(&body.user_id) { - return Err!(Request(InvalidParam("Can only set the suspended status of local users"))); - } - if !active { - return Err!(Request(NotFound("Unknown user"))); - } + + status.ensure_active()?; + if body.user_id == *sender_user { return Err!(Request(Forbidden("You cannot suspend yourself"))); } + if target_admin { return Err!(Request(Forbidden("You cannot suspend another server administrator"))); } + if services.users.is_suspended(&body.user_id).await? == body.suspended { // No change return Ok(suspend_user::v1::Response::new(body.suspended)); diff --git a/src/api/client/capabilities.rs b/src/api/client/capabilities.rs index 522cbbd1c..871786ab8 100644 --- a/src/api/client/capabilities.rs +++ b/src/api/client/capabilities.rs @@ -7,10 +7,11 @@ use ruma::{ api::client::discovery::get_capabilities::{ self, v3::{ - Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability, - ThirdPartyIdChangesCapability, + Capabilities, GetLoginTokenCapability, ProfileFieldsCapability, RoomVersionStability, + RoomVersionsCapability, ThirdPartyIdChangesCapability, }, }, + assign, }; use crate::Ruma; @@ -50,5 +51,9 @@ pub(crate) async fn get_capabilities_route( capabilities.account_moderation.suspend = true; } + capabilities.profile_fields = Some( + assign!(ProfileFieldsCapability::new(true), { disallowed: Some(services.oidc.restricted_profile_fields()) }), + ); + Ok(get_capabilities::v3::Response::new(capabilities)) } diff --git a/src/api/client/oauth/mod.rs b/src/api/client/oauth/mod.rs index 865332950..0c3e8fa4a 100644 --- a/src/api/client/oauth/mod.rs +++ b/src/api/client/oauth/mod.rs @@ -37,7 +37,7 @@ pub(crate) fn router(state: crate::State) -> Router { .layer(middleware::from_fn_with_state( state, async |State(state): State, request: Request, next: Next| -> Response { - if state.config.oauth.compatibility_mode.oauth_available() { + if state.config.oauth.compatibility_mode().oauth_available() { next.run(request).await } else { (StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response() diff --git a/src/api/client/oauth/server_metadata.rs b/src/api/client/oauth/server_metadata.rs index 60d287960..eaf4d2a7a 100644 --- a/src/api/client/oauth/server_metadata.rs +++ b/src/api/client/oauth/server_metadata.rs @@ -21,7 +21,7 @@ pub(crate) async fn get_authorization_server_metadata_route( State(services): State, _body: Ruma, ) -> Result { - if !services.config.oauth.compatibility_mode.oauth_available() { + if !services.config.oauth.compatibility_mode().oauth_available() { return Err!(Request(Unrecognized("OAuth is unavailable on this server"))); } diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 7dd3e6371..5739f3781 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -1,9 +1,8 @@ use std::collections::BTreeMap; use axum::extract::State; -use conduwuit::{Err, Result, matrix::pdu::PartialPdu, utils::to_canonical_object}; +use conduwuit::{Err, Result}; use conduwuit_service::Services; -use futures::StreamExt; use ruma::{ UserId, api::{ @@ -13,11 +12,10 @@ use ruma::{ federation, }, assign, - events::room::member::MembershipState, - presence::PresenceState, profile::{ProfileFieldName, ProfileFieldValue}, }; -use serde_json::{Value, to_value}; +use serde_json::Value; +use service::users::ProfileFieldChange; use crate::Ruma; @@ -65,13 +63,24 @@ pub(crate) async fn set_profile_field_route( return Err!(Request(InvalidParam("You may not change a remote user's profile data."))); } - set_profile_field( - &services, - &body.user_id, - ProfileFieldChange::Set(body.value.clone()), - body.propagate_to.clone(), - ) - .await?; + if services + .oidc + .restricted_profile_fields() + .contains(&body.value.field_name()) + { + return Err!(Request(Forbidden( + "This profile field is controlled by your identity provider." + ))); + } + + services + .users + .set_profile_field( + &body.user_id, + ProfileFieldChange::Set(body.value.clone()), + body.propagate_to.clone(), + ) + .await?; Ok(set_profile_field::v3::Response::new()) } @@ -94,13 +103,24 @@ pub(crate) async fn delete_profile_field_route( return Err!(Request(InvalidParam("You may not change a remote user's profile data."))); } - set_profile_field( - &services, - &body.user_id, - ProfileFieldChange::Delete(body.field.clone()), - body.propagate_to.clone(), - ) - .await?; + if services + .oidc + .restricted_profile_fields() + .contains(&body.field) + { + return Err!(Request(Forbidden( + "This profile field is controlled by your identity provider." + ))); + } + + services + .users + .set_profile_field( + &body.user_id, + ProfileFieldChange::Delete(body.field.clone()), + body.propagate_to.clone(), + ) + .await?; Ok(delete_profile_field::v3::Response::new()) } @@ -110,8 +130,8 @@ async fn fetch_full_profile( user_id: &UserId, ) -> Option> { // If the user exists locally, fetch their local profile - if services.users.exists(user_id).await { - return Some(get_local_profile(services, user_id).await); + if services.users.status(user_id).await.is_found() { + return Some(services.users.get_local_profile(user_id).await); } // Otherwise ask their homeserver @@ -135,13 +155,10 @@ async fn fetch_full_profile( continue; }; - let _ = set_profile_field( - services, - user_id, - ProfileFieldChange::Set(value), - PropagateTo::None, - ) - .await; + let _ = services + .users + .set_profile_field(user_id, ProfileFieldChange::Set(value), PropagateTo::None) + .await; } Some(BTreeMap::from_iter(response)) @@ -154,7 +171,7 @@ async fn fetch_profile_field( ) -> Result> { // If the user exists locally, fetch their local profile field if services.globals.user_is_local(user_id) { - return Ok(get_local_profile_field(services, user_id, field).await); + return Ok(services.users.get_local_profile_field(user_id, field).await); } // Otherwise ask their homeserver @@ -175,13 +192,14 @@ async fn fetch_profile_field( if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) { if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) { - let _ = set_profile_field( - services, - user_id, - ProfileFieldChange::Set(value.clone()), - PropagateTo::None, - ) - .await; + let _ = services + .users + .set_profile_field( + user_id, + ProfileFieldChange::Set(value.clone()), + PropagateTo::None, + ) + .await; Ok(Some(value)) } else { @@ -190,260 +208,11 @@ async fn fetch_profile_field( ))) } } else { - let _ = set_profile_field( - services, - user_id, - ProfileFieldChange::Delete(field), - PropagateTo::None, - ) - .await; + let _ = services + .users + .set_profile_field(user_id, ProfileFieldChange::Delete(field), PropagateTo::None) + .await; Ok(None) } } - -pub(crate) async fn get_local_profile( - services: &Services, - user_id: &UserId, -) -> BTreeMap { - let mut profile = BTreeMap::new(); - - // Get displayname and avatar_url independently because `all_profile_keys` - // doesn't include them - for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] { - let key = field.as_str().to_owned(); - - if let Some(value) = get_local_profile_field(services, user_id, field).await { - profile.insert(key, value.value().into_owned()); - } - } - - // Insert all other profile fields - let mut all_fields = services.users.all_profile_keys(user_id); - - while let Some((key, value)) = all_fields.next().await { - profile.insert(key, value); - } - - profile -} - -pub(crate) async fn get_local_profile_field( - services: &Services, - user_id: &UserId, - field: ProfileFieldName, -) -> Option { - let value = match field.clone() { - | ProfileFieldName::AvatarUrl => services - .users - .avatar_url(user_id) - .await - .ok() - .map(to_value) - .transpose() - .expect("converting avatar url to value should succeed"), - | ProfileFieldName::DisplayName => services - .users - .displayname(user_id) - .await - .ok() - .map(to_value) - .transpose() - .expect("converting displayname to value should succeed"), - | other => services - .users - .profile_key(user_id, other.as_str()) - .await - .ok(), - }?; - - Some( - ProfileFieldValue::new(field.as_str(), value) - .expect("local profile field should be valid"), - ) -} - -enum ProfileFieldChange { - Set(ProfileFieldValue), - Delete(ProfileFieldName), -} - -impl ProfileFieldChange { - fn field_name(&self) -> ProfileFieldName { - match self { - | &Self::Delete(ref name) => name.clone(), - | &Self::Set(ref value) => value.field_name(), - } - } - - fn value(&self) -> Option { - if let Self::Set(value) = self { - Some(value.value().into_owned()) - } else { - None - } - } -} - -async fn set_profile_field( - services: &Services, - user_id: &UserId, - change: ProfileFieldChange, - propagate_to: PropagateTo, -) -> Result<()> { - const MAX_KEY_LENGTH_BYTES: usize = 255; - const MAX_PROFILE_LENGTH_BYTES: usize = 65536; - - let field_name = change.field_name(); - - // TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE, - // M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list - // of error codes and Ruma doesn't have them. Should we return those, or is - // M_TOO_LARGE okay? - if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES { - return Err!(Request(TooLarge( - "Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length." - ))); - } - - // Serialize the entire profile as canonical JSON, including the new change, - // to check if it exceeds 64 KiB - { - let mut full_profile = get_local_profile(services, user_id).await; - - match &change { - | ProfileFieldChange::Set(value) => { - full_profile.insert( - value.field_name().as_str().to_owned(), - value.value().clone().into_owned(), - ); - }, - | ProfileFieldChange::Delete(key) => { - full_profile.remove(key.as_str()); - }, - } - - if let Ok(canonical_profile) = to_canonical_object(full_profile) { - if serde_json::to_string(&canonical_profile) - .expect("should be able to serialize to string") - .len() > MAX_PROFILE_LENGTH_BYTES - { - return Err!( - "Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in length." - ); - } - } else { - return Err!(Request(BadJson("Failed to canonicalize profile."))); - } - } - - // If the user is local and changed their displayname or avatar_url, update it - // in all their joined rooms. This is done before updating their profile data - // so we can check the old value of the field if `propagate_to` is `unchanged`. - if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName) - && matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged) - && services.globals.user_is_local(user_id) - { - let current_displayname = services.users.displayname(user_id).await.ok(); - let current_avatar_url = services.users.avatar_url(user_id).await.ok(); - - let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id); - - while let Some(room_id) = all_joined_rooms.next().await { - // TODO: this clobbers any custom fields on the event content - let mut current_membership = services - .rooms - .state_accessor - .get_member(&room_id, user_id) - .await - .expect("should be able to fetch membership event for joined room"); - - assert_eq!( - current_membership.membership, - MembershipState::Join, - "user should be joined" - ); - - // If `propagate_to` is `unchanged`, and the current value of the field we're - // updating was changed from its global value in this room, skip it. - if matches!(propagate_to, PropagateTo::Unchanged) { - let field_changed_from_global = match field_name { - | ProfileFieldName::AvatarUrl => - current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(), - | ProfileFieldName::DisplayName => - current_membership.displayname.as_ref() != current_displayname.as_ref(), - | _ => unreachable!(), - }; - - if field_changed_from_global { - continue; - } - } - - let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await; - - // Preserve keys in accordance with the key copying rules - current_membership.reason = None; - current_membership.join_authorized_via_users_server = None; - match &change { - | ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => { - current_membership.avatar_url = Some(avatar_url.clone()); - }, - | ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => { - current_membership.displayname = Some(displayname.clone()); - }, - | ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => { - current_membership.avatar_url = None; - }, - | ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => { - current_membership.displayname = None; - }, - | _ => unreachable!(), - } - - let _ = services - .rooms - .timeline - .build_and_append_pdu( - PartialPdu::state(user_id.to_string(), ¤t_membership), - user_id, - Some(&room_id), - &state_lock, - ) - .await; - } - - if services.config.allow_local_presence { - // Send a presence EDU to indicate the profile changed - let _ = services - .presence - .ping_presence(user_id, &PresenceState::Online) - .await; - } - } - - match change { - | ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => { - services - .users - .set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty())); - }, - | ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => { - services - .users - .set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid())); - }, - | ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => { - services.users.set_displayname(user_id, None); - }, - | ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => { - services.users.set_avatar_url(user_id, None); - }, - | other => - services - .users - .set_profile_key(user_id, other.field_name().as_str(), other.value()), - } - - Ok(()) -} diff --git a/src/api/client/report.rs b/src/api/client/report.rs index a7567c113..a7acd9790 100644 --- a/src/api/client/report.rs +++ b/src/api/client/report.rs @@ -149,7 +149,7 @@ pub(crate) async fn report_user_route( delay_response().await; - if !services.users.is_active_local(&body.user_id).await { + if !services.users.status(&body.user_id).await.is_found() { // return 200 as to not reveal if the user exists. Recommended by spec. return Ok(report_user::v3::Response::new()); } diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index 259fb9a12..5a9d87cc9 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -337,14 +337,21 @@ pub(crate) async fn create_room_route( }); let mut power_levels_to_grant = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]); + let mut creators: Vec = vec![sender_user.to_owned()]; if preset == RoomPreset::TrustedPrivateChat { - for recipient_user in &invitees { - power_levels_to_grant.insert(recipient_user.clone(), int!(100)); + for recipient_user in invitees.iter().cloned() { + if room_version_rules + .authorization + .explicitly_privilege_room_creators + { + creators.push(recipient_user); + } else { + power_levels_to_grant.insert(recipient_user, int!(100)); + } } } - let mut creators: Vec = vec![sender_user.to_owned()]; // Do we care about additional_creators? if room_version_rules .authorization diff --git a/src/api/client/session.rs b/src/api/client/session.rs index dd95a1c2c..fa04b4242 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -43,7 +43,7 @@ pub(crate) async fn get_login_types_route( ClientIp(client): ClientIp, _body: Ruma, ) -> Result { - if !services.config.oauth.compatibility_mode.uiaa_available() { + if !services.config.oauth.compatibility_mode().uiaa_available() { return Err!(Request(Unrecognized( "User-interactive authentication is not available on this server." ))); @@ -88,14 +88,6 @@ pub async fn handle_login( UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name) .map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?; - if !services.globals.user_is_local(&user_id) { - return Err!(Request(InvalidParam("User ID does not belong to this homeserver"))); - } - - if services.users.is_deactivated(&user_id).await? { - return Err!(Request(UserDeactivated("This account has been deactivated."))); - } - if services.users.is_locked(&user_id).await? { return Err!(Request(UserLocked("This account has been locked."))); } @@ -128,7 +120,7 @@ pub(crate) async fn login_route( ClientIp(client): ClientIp, body: Ruma, ) -> Result { - if !services.config.oauth.compatibility_mode.uiaa_available() { + if !services.config.oauth.compatibility_mode().uiaa_available() { return match body.login_info { | LoginInfo::ApplicationService(_) => { Err!(Request(AppserviceLoginUnsupported( diff --git a/src/api/client/user_directory.rs b/src/api/client/user_directory.rs index 18c02163b..7d8a9a9c4 100644 --- a/src/api/client/user_directory.rs +++ b/src/api/client/user_directory.rs @@ -32,22 +32,26 @@ pub(crate) async fn search_users_route( .min(LIMIT_MAX); let search_term = body.search_term.to_lowercase(); - let mut users = services.users.stream().broad_filter_map(async |user_id| { - let display_name = services.users.displayname(&user_id).await.ok(); - let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term); + let mut users = services + .users + .stream_local_users() + .chain(services.users.stream_remote_users()) + .broad_filter_map(async |user_id| { + let display_name = services.users.displayname(&user_id).await.ok(); - let display_name_matches = display_name - .as_deref() - .map(str::to_lowercase) - .is_some_and(|display_name| display_name.contains(&search_term)); + let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term); - if !user_id_matches && !display_name_matches { - return None; - } + let display_name_matches = display_name + .as_deref() + .map(str::to_lowercase) + .is_some_and(|display_name| display_name.contains(&search_term)); - let user_in_public_room = - services + if !user_id_matches && !display_name_matches { + return None; + } + + let user_in_public_room = services .rooms .state_cache .rooms_joined(&user_id) @@ -60,22 +64,22 @@ pub(crate) async fn search_users_route( .await }); - let user_sees_user = services - .rooms - .state_cache - .user_sees_user(sender_user, &user_id); + let user_sees_user = services + .rooms + .state_cache + .user_sees_user(sender_user, &user_id); - pin_mut!(user_in_public_room, user_sees_user); + pin_mut!(user_in_public_room, user_sees_user); - if user_in_public_room.or(user_sees_user).await { - Some(assign!(search_users::v3::User::new(user_id.clone()), { - display_name, - avatar_url: services.users.avatar_url(&user_id).await.ok(), - })) - } else { - None - } - }); + if user_in_public_room.or(user_sees_user).await { + Some(assign!(search_users::v3::User::new(user_id.clone()), { + display_name, + avatar_url: services.users.avatar_url(&user_id).await.ok(), + })) + } else { + None + } + }); let results = users.by_ref().take(limit).collect().await; let limited = users.next().await.is_some(); diff --git a/src/api/server/query.rs b/src/api/server/query.rs index ff19d8214..dccbce067 100644 --- a/src/api/server/query.rs +++ b/src/api/server/query.rs @@ -7,10 +7,7 @@ use ruma::{ api::federation::query::{get_profile_information, get_room_information}, }; -use crate::{ - Ruma, - client::{get_local_profile, get_local_profile_field}, -}; +use crate::Ruma; /// # `GET /_matrix/federation/v1/query/directory` /// @@ -75,15 +72,19 @@ pub(crate) async fn get_profile_information_route( let response = if let Some(field) = &body.field { let mut response = get_profile_information::v1::Response::new(); - if let Some(value) = - get_local_profile_field(&services, &body.user_id, field.to_owned()).await + if let Some(value) = services + .users + .get_local_profile_field(&body.user_id, field.to_owned()) + .await { response.set(value.field_name().as_str().to_owned(), value.value().into_owned()); } response } else { - get_local_profile(&services, &body.user_id) + services + .users + .get_local_profile(&body.user_id) .await .into_iter() .collect() diff --git a/src/api/server/send.rs b/src/api/server/send.rs index 47438ae85..ead1713c1 100644 --- a/src/api/server/send.rs +++ b/src/api/server/send.rs @@ -620,8 +620,9 @@ async fn handle_edu_direct_to_device( .broad_filter_map(|(target_user_id, map)| async move { services .users - .is_active_local(&target_user_id) + .status(&target_user_id) .await + .is_active() .then_some((target_user_id, map)) }) .for_each_concurrent(automatic_width(), |(target_user_id, map)| { diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index e918285c1..1daf731a6 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -119,6 +119,7 @@ parking_lot.workspace = true lock_api.workspace = true hyper-util.workspace = true resolvematrix.workspace = true +openidconnect.workspace = true [target.'cfg(unix)'.dependencies] nix.workspace = true diff --git a/src/core/config/check.rs b/src/core/config/check.rs index bc9878598..840413823 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -289,6 +289,15 @@ pub fn check(config: &Config) -> Result { | _ => (), } + if let Some(oidc) = &config.oauth.oidc + && oidc.client_secret.is_none() + && oidc.client_secret_file.is_none() + { + return Err!( + "Either `client_secret` or `client_secret_file` must be set if OIDC is configured." + ); + } + Ok(()) } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 4e81c32bd..e56dcbf0b 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -4,7 +4,7 @@ pub mod manager; pub mod proxy; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::PathBuf, }; @@ -17,10 +17,12 @@ use either::{ use figment::providers::{Env, Format, Toml}; pub use figment::{Figment, value::Value as FigmentValue}; use lettre::message::Mailbox; +use openidconnect::{ClientId, ClientSecret, Scope}; use regex::RegexSet; use ruma::{ OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId, api::client::{discovery::discover_support::ContactRole, rtc::RtcTransport}, + profile::ProfileFieldName, serde::Base64, }; use serde::{Deserialize, Serialize, de::IgnoredAny}; @@ -2419,10 +2421,24 @@ pub struct OauthConfig { /// legacy authentication will be unable to log in. /// /// default: "hybrid" - pub compatibility_mode: OAuthMode, + compatibility_mode: OAuthMode, + + /// display: hidden + pub oidc: Option, } -#[derive(Clone, Debug, Default, Deserialize)] +impl OauthConfig { + #[must_use] + pub fn compatibility_mode(&self) -> OAuthMode { + if self.oidc.is_some() { + OAuthMode::Exclusive + } else { + self.compatibility_mode + } + } +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OAuthMode { Disabled, @@ -2439,6 +2455,121 @@ impl OAuthMode { pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) } } +#[derive(Clone, Debug, Deserialize)] +#[config_example_generator( + filename = "conduwuit-example.toml", + section = "global.oauth.oidc", + optional = "true", + subheader = "\ +# Uncommenting this section will enable Continuwuity's support for +# authenticating users using an OpenID Connect-compatible identity provider. +# This is referred to as \"delegated authentication\". +# +# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if +# the `global.oauth.compatibility_mode` setting is set to `exclusive`. +# Matrix clients which do not support OAuth login (also referred to as \"next-gen auth\") will \ + NOT be able +# to log in while delegated authentication is active." +)] +pub struct OidcConfig { + /// The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to + /// automatically fetch the identity provider's metadata from this URL. + /// Generally you should set this to the base domain your identity provider + /// runs on. + pub discovery_url: Url, + + /// The OAuth client ID for Continuwuity to use when communicating with the + /// identity provider. + pub client_id: ClientId, + + /// The OAuth client secret for Continuwuity to use when communicating with + /// the identity provider. + pub client_secret: Option, + + /// A path to a file which Continuwuity will read the client secret from. + /// If this option is set, it will override `client_secret`. + /// + /// The server will fail to start if the file cannot be read. + pub client_secret_file: Option, + + /// Additional scopes Continuwuity should request from the IDP. This may be + /// necessary to access certain claims. Continuwuity always requests the + /// `openid` scope. + /// + /// default: [] + #[serde(default)] + pub additional_scopes: Vec, + + /// Whether the user should be prompted to choose a localpart + /// when signing in for the first time. If this is `false`, Continuwuity + /// will attempt to use the value of the `preferred_username_claim` + /// (see below) as the user's localpart. Authentication will + /// fail if this claim is missing or is not a valid localpart. + /// + /// default: true + #[serde(default = "true_fn")] + pub prompt_for_localpart: bool, + + /// The claim to use for the user's localpart, if `prompt_for_localpart` is + /// false. + /// + /// default: "preferred_username" + #[serde(default = "default_preferred_username_claim")] + pub preferred_username_claim: String, + + /// The claim which will be used to set the user's email address, + /// either on initial registration or on every login depending on + /// the value of `profile_key_import_mode`. Continuwuity assumes that + /// the IDP has taken care of verifying that the user controls the email + /// address it provides. + /// + /// This option does nothing if SMTP is not configured. + /// + /// If this option is set, and `profile_key_import_mode` is `on_login`, + /// users will not be able to change their email addresses themselves. + /// + /// default: "email" + pub email_claim: Option, + + /// Defines how claims returned from the IDP should be mapped to a user's + /// profile data. The profile field named in each key will be set from the + /// claim named in the corresponding value when the user first registers, + /// and possibly on subsequent logins as well, depending on the value of + /// `profile_key_import_mode` (see below). + /// + /// Per-room overrides to the user's display name or avatar will be + /// preserved by the import process. + /// + /// SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will + /// perform a HTTP GET to the URL in the mapped claim and use the returned + /// file as the user's profile picture. Make sure your users are not able + /// to set the value of the mapped claim to an arbitrary URL. + /// + /// default: { displayname = "name" } + #[serde(default = "default_profile_key_map")] + pub profile_key_map: HashMap, + + /// When profile keys should be imported from the IDP's claims. + /// + /// - "on_registration": Listed keys will be imported once, when the user + /// logs in for the first time and their shadow account is created. + /// - "on_login": Listed keys will be imported every time the user logs in. + /// Additionally, users will not be able to manually edit any listed keys + /// through their Matrix client. + /// + /// default: "on_registration" + #[serde(default)] + pub profile_key_import_mode: OidcProfileKeyImportMode, +} + +#[derive(Clone, Debug, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum OidcProfileKeyImportMode { + #[default] + OnRegistration, + OnLogin, +} + const DEPRECATED_KEYS: &[&str] = &[ "cache_capacity", "conduit_cache_capacity_modifier", @@ -2823,3 +2954,9 @@ fn default_client_shutdown_timeout() -> u64 { 15 } fn default_sender_shutdown_timeout() -> u64 { 5 } fn default_terms_language() -> String { "en".to_owned() } + +fn default_preferred_username_claim() -> String { "preferred_username".to_owned() } + +fn default_profile_key_map() -> HashMap { + HashMap::from_iter([(ProfileFieldName::DisplayName.to_string(), "name".to_owned())]) +} diff --git a/src/database/maps.rs b/src/database/maps.rs index 778c098e8..40336ee17 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -124,6 +124,14 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "onetimekeyid_onetimekeys", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "openidsubject_localpart", + ..descriptor::RANDOM_SMALL + }, + Descriptor { + name: "openidsubject_currentpictureurl", + ..descriptor::RANDOM_SMALL + }, Descriptor { name: "fallbackkeyid_fallbackkey", ..descriptor::RANDOM_SMALL @@ -169,6 +177,10 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "registrationtoken_info", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "remoteuserid_remoteuser", + ..descriptor::RANDOM + }, Descriptor { name: "roomid_invitedcount", ..descriptor::RANDOM_SMALL @@ -398,6 +410,10 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userid_blurhash", ..descriptor::DROPPED }, + Descriptor { + name: "userid_deactivated", + ..descriptor::RANDOM_SMALL + }, Descriptor { name: "userid_dehydrateddevice", ..descriptor::RANDOM_SMALL diff --git a/src/macros/config.rs b/src/macros/config.rs index b0b42eddd..c2e4818d9 100644 --- a/src/macros/config.rs +++ b/src/macros/config.rs @@ -81,6 +81,12 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result = Vec::new(); diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 923fef2cb..549a3cdb6 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -121,6 +121,7 @@ yansi.workspace = true lettre.workspace = true resolvematrix.workspace = true serde_urlencoded.workspace = true +openidconnect.workspace = true [target.'cfg(all(unix, target_os = "linux"))'.dependencies] sd-notify.workspace = true diff --git a/src/service/admin/create.rs b/src/service/admin/create.rs index 46c838f9d..2febccaa4 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -48,7 +48,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_shadow_account(server_user).await?; let mut create_content = if room_version_rules.authorization.use_room_create_sender { RoomCreateEventContent::new_v1(server_user.into()) diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index ebd73d8af..3c3efa9bc 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -108,17 +108,17 @@ impl Service { self.services.globals.server_name(), )?; - if !self.services.users.exists(&appservice_user_id).await { - self.services.users.create(&appservice_user_id, None)?; - } else if self + if !self .services .users - .is_deactivated(&appservice_user_id) + .status(&appservice_user_id) .await - .unwrap_or(false) + .is_found() { - // Reactivate the appservice user if it was accidentally deactivated - self.services.users.set_password(&appservice_user_id, None); + self.services + .users + .create_shadow_account(&appservice_user_id) + .await?; } self.registration_info diff --git a/src/service/emergency/mod.rs b/src/service/emergency/mod.rs index 31fa8604c..eca3ccbf8 100644 --- a/src/service/emergency/mod.rs +++ b/src/service/emergency/mod.rs @@ -54,15 +54,22 @@ impl Service { async fn set_emergency_access(&self) -> Result { let server_user = &self.services.globals.server_user; - self.services.users.set_password( - server_user, - self.services - .config - .emergency_password - .as_deref() - .map(HashedPassword::new) - .transpose()?, - ); + match &self.services.config.emergency_password { + | Some(emergency_password) => { + let emergency_password = HashedPassword::new(emergency_password)?; + + self.services + .users + .convert_to_local_account(server_user, emergency_password) + .await?; + }, + | None => { + self.services + .users + .convert_to_shadow_account(server_user) + .await?; + }, + } let (ruleset, pwd_set) = match self.services.config.emergency_password { | Some(_) => (Ruleset::server_default(server_user), true), diff --git a/src/service/media/preview.rs b/src/service/media/preview.rs index 54b321fba..2d08e793a 100644 --- a/src/service/media/preview.rs +++ b/src/service/media/preview.rs @@ -211,7 +211,6 @@ impl Service { Ok(preview_data) } - #[cfg(feature = "url_preview")] pub async fn download_media(&self, url: &str) -> Result<(OwnedMxcUri, usize)> { use conduwuit::utils::random_string; use http::header::CONTENT_TYPE; @@ -268,11 +267,6 @@ impl Service { Err!(FeatureDisabled("url_preview")) } - #[cfg(not(feature = "url_preview"))] - pub async fn download_media(&self, _url: &str) -> Result { - Err!(FeatureDisabled("url_preview")) - } - #[cfg(feature = "url_preview")] async fn download_html(&self, url: &str) -> Result { use webpage::HTML; diff --git a/src/service/migrations.rs b/src/service/migrations.rs index bd087289b..f62aa21de 100644 --- a/src/service/migrations.rs +++ b/src/service/migrations.rs @@ -41,7 +41,7 @@ pub(crate) async fn migrations(services: &Services) -> Result<()> { // requires recreating the database from scratch. if users_count > 0 { let server_user = &services.globals.server_user; - if !services.users.exists(server_user).await { + if !services.users.status(server_user).await.is_found() { error!("The {server_user} server user does not exist, and the database is not new."); return Err!(Database( "Cannot reuse an existing database after changing the server name, please \ @@ -228,6 +228,18 @@ async fn migrate(services: &Services) -> Result<()> { .map_err(|e| err!("Failed to run 'fix_local_invite_state' migration': {e}"))?; } + if services.globals.db.database_version().await < 18 { + services.globals.db.bump_database_version(18); + info!("Migration: Bumped database version to 18"); + } + + if db["global"].get(SPLIT_USERID_PASSWORD).await.is_not_found() { + info!("Running migration 'split_userid_password'"); + split_userid_password(services) + .await + .map_err(|e| err!("Failed to run 'split_userid_password' migration': {e}"))?; + } + assert_eq!( services.globals.db.database_version().await, DATABASE_VERSION, @@ -242,9 +254,9 @@ async fn migrate(services: &Services) -> Result<()> { if !patterns.is_empty() { services .users - .stream() + .stream_local_users() .filter_map(async |user_id| { - if services.users.is_active_local(&user_id).await { + if services.users.status(&user_id).await.is_found() { Some(user_id) } else { None @@ -774,7 +786,7 @@ async fn fix_local_invite_state(services: &Services) -> Result { let db = &services.db; let cork = db.cork_and_sync(); - let userroomid_invitestate = services.db["userroomid_invitestate"].clone(); + let userroomid_invitestate = db["userroomid_invitestate"].clone(); // for each user invited to a room let fixed = userroomid_invitestate.stream() @@ -818,3 +830,44 @@ async fn fix_local_invite_state(services: &Services) -> Result { db.db.sort()?; Ok(()) } + +const SPLIT_USERID_PASSWORD: &str = "split_userid_password"; +async fn split_userid_password(services: &Services) -> Result { + // Split remote and deactivated users out from the `userid_password` table + + let db = &services.db; + let cork = db.cork_and_sync(); + let userid_password = db["userid_password"].clone(); + let remoteuserid_remoteuser = db["remoteuserid_remoteuser"].clone(); + let userid_deactivated = db["userid_deactivated"].clone(); + + let remote_users = userid_password + .stream::() + .ignore_err() + .fold(0_usize, async |mut remote_users, (user_id, hash)| { + if !services.globals.user_is_local(&user_id) { + assert!(hash.is_empty(), "non-empty hash {hash} for remote user {user_id}"); + + remoteuserid_remoteuser.insert(&user_id, ""); + userid_password.remove(&user_id); + remote_users = remote_users.saturating_add(1); + } else if hash.is_empty() { + if !(services.appservice.is_exclusive_user_id(&user_id).await + || user_id == services.globals.server_user) + { + info!("Marking {user_id} as deactivated"); + userid_deactivated.insert(&user_id, ""); + } + } + + remote_users + }) + .await; + + drop(cork); + info!(?remote_users, "Split userid_password."); + + db["global"].insert(FIXED_LOCAL_INVITE_STATE_MARKER, []); + db.db.sort()?; + Ok(()) +} diff --git a/src/service/mod.rs b/src/service/mod.rs index 8471891a2..5488ff178 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -28,6 +28,7 @@ pub mod mailer; pub mod media; pub mod moderation; pub mod oauth; +pub mod oidc; pub mod presence; pub mod pusher; pub mod registration_tokens; diff --git a/src/service/oauth/grant.rs b/src/service/oauth/grant.rs index d1dd8efc1..0a027f637 100644 --- a/src/service/oauth/grant.rs +++ b/src/service/oauth/grant.rs @@ -160,7 +160,7 @@ pub enum ErrorCode { InvalidClientMetadata, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] pub struct AuthorizationCodeResponse { pub state: String, pub code: String, diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs new file mode 100644 index 000000000..507cbd828 --- /dev/null +++ b/src/service/oidc/mod.rs @@ -0,0 +1,484 @@ +use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration}; + +use async_trait::async_trait; +use conduwuit::{ + Result, + config::{OidcConfig, OidcProfileKeyImportMode}, + debug, err, error, info, warn, +}; +use database::{Deserialized, Map}; +use lettre::Address; +use openidconnect::{ + AdditionalClaims, AuthorizationCode, ClientSecret, CsrfToken, EmptyExtraTokenFields, + EndpointMaybeSet, EndpointNotSet, EndpointSet, IdTokenClaims, IdTokenFields, IssuerUrl, + Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, StandardErrorResponse, + StandardTokenResponse, TokenResponse, + core::{ + CoreAuthDisplay, CoreAuthPrompt, CoreAuthenticationFlow, CoreErrorResponseType, + CoreGenderClaim, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreRevocableToken, + CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType, + }, + reqwest, +}; +use ruma::{ + OwnedUserId, UserId, + api::client::profile::PropagateTo, + profile::{ProfileFieldName, ProfileFieldValue}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tokio::{runtime, sync::SetOnce}; +use url::Url; + +use crate::{ + Dep, config, globals, media, + oauth::grant::AuthorizationCodeResponse, + threepid, + users::{self, AccountStatus, ProfileFieldChange}, +}; + +pub struct Service { + services: Services, + runtime: runtime::Handle, + db: Data, + client: Option, +} + +struct Data { + openidsubject_localpart: Arc, + openidsubject_currentpictureurl: Arc, +} +struct Services { + config: Dep, + globals: Dep, + media: Dep, + threepid: Dep, + users: Dep, +} + +struct OidcClient { + config: OidcConfig, + client_secret: ClientSecret, + machine: SetOnce, + client: reqwest::Client, +} + +type OidcClientMachine = openidconnect::Client< + AllClaims, + CoreAuthDisplay, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJsonWebKey, + CoreAuthPrompt, + StandardErrorResponse, + StandardTokenResponse< + IdTokenFields< + AllClaims, + EmptyExtraTokenFields, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, + >, + CoreTokenType, + >, + CoreTokenIntrospectionResponse, + CoreRevocableToken, + CoreRevocationErrorResponse, + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointMaybeSet, + EndpointMaybeSet, +>; + +pub type Claims = IdTokenClaims; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AllClaims { + #[serde(flatten)] + pub claims: HashMap, +} + +impl AdditionalClaims for AllClaims {} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PendingSession { + pkce_verifier: PkceCodeVerifier, + nonce: Nonce, + csrf_token: CsrfToken, +} + +pub enum SessionCompletionStatus { + NeedsUserId, + Complete(OwnedUserId), +} + +#[async_trait] +impl crate::Service for Service { + fn build(args: crate::Args<'_>) -> Result> { + Ok(Arc::new(Self { + services: Services { + config: args.depend::("config"), + globals: args.depend::("globals"), + media: args.depend::("media"), + threepid: args.depend::("threepid"), + users: args.depend::("users"), + }, + runtime: args.server.runtime().clone(), + db: Data { + openidsubject_localpart: args.db["openidsubject_localpart"].clone(), + openidsubject_currentpictureurl: args.db["openidsubject_currentpictureurl"].clone(), + }, + client: args.server.config.oauth.oidc.as_ref().map(|config| -> Result { + Ok(OidcClient { + config: config.clone(), + client_secret: if let Some(client_secret_file) = &config.client_secret_file { + std::fs::read_to_string(client_secret_file) + .map(ClientSecret::new) + .map_err(|err| err!("Failed to read OIDC client secret file: {err}"))? + } else if let Some(client_secret) = &config.client_secret { + client_secret.clone() + } else { + // The config check function should cause an early exit before this happens + panic!("neither client secret or client secret file were set"); + }, + machine: SetOnce::new(), + // This isn't in the client service because it has to use the `reqwest` shipped by `openidconnect` + client: reqwest::ClientBuilder::new() + .connect_timeout(Duration::from_secs(args.server.config.request_conn_timeout)) + .read_timeout(Duration::from_secs(args.server.config.request_timeout)) + .timeout(Duration::from_secs(args.server.config.request_total_timeout)) + .pool_idle_timeout(Duration::from_secs(args.server.config.request_idle_timeout)) + .pool_max_idle_per_host(args.server.config.request_idle_per_host.into()) + .user_agent(conduwuit::user_agent()) + .redirect(reqwest::redirect::Policy::none()) + .danger_accept_invalid_certs(args.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure) + .build() + .expect("client should build") + })} + ).transpose()?, + })) + } + + async fn worker(self: Arc) -> Result { + if let Some(OidcClient { config, client_secret, machine, client }) = &self.client { + let redirect_url = self + .services + .config + .get_client_domain() + .join(&format!("{}/oidc/complete", conduwuit::ROUTE_PREFIX)) + .expect("redirect url should be valid"); + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::from_url(config.discovery_url.clone()), + client, + ) + .await + .map_err(|err| err!("Failed to discover OIDC provider metadata: {err}"))?; + + machine + .set( + OidcClientMachine::from_provider_metadata( + provider_metadata, + config.client_id.clone(), + Some(client_secret.clone()), + ) + .set_redirect_uri(RedirectUrl::from_url(redirect_url)), + ) + .expect("machine should be empty"); + } + + Ok(()) + } + + fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } +} + +impl Service { + const SERVER_MISCONFIGURED: &str = + "Identity server is misconfigured. Contact your homeserver's administrator."; + + pub fn enabled(&self) -> bool { self.client.is_some() } + + pub fn restricted_profile_fields(&self) -> Vec { + if let Some(config) = self.client.as_ref().map(|client| &client.config) + && matches!(config.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin) + { + config + .profile_key_map + .keys() + .map(|key| ProfileFieldName::from(key.as_str())) + .collect() + } else { + vec![] + } + } + + pub async fn begin_session(&self, prompt: Option) -> (PendingSession, Url) { + let OidcClient { machine, config, .. } = + self.client.as_ref().expect("oidc should be configured"); + let machine = machine.wait().await; + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + let mut auth_url = machine + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scopes(config.additional_scopes.iter().cloned()) + .set_pkce_challenge(pkce_challenge); + + if let Some(prompt) = prompt { + auth_url = auth_url.add_prompt(prompt); + } + + let (auth_url, csrf_token, nonce) = auth_url.url(); + + (PendingSession { pkce_verifier, nonce, csrf_token }, auth_url) + } + + pub async fn exchange_code( + &self, + session: PendingSession, + response: AuthorizationCodeResponse, + ) -> Result { + let Some(OidcClient { machine, client, .. }) = self.client.as_ref() else { + return Err("Delegated authentication is not enabled on this server."); + }; + + let machine = machine.wait().await; + + if session.csrf_token.into_secret() != response.state { + return Err("State mismatch."); + } + + let token_response = machine + .exchange_code(AuthorizationCode::new(response.code)) + .expect("machine should be configured correctly") + .set_pkce_verifier(session.pkce_verifier) + .request_async(client) + .await + .map_err(|err| { + error!("Failed to exchange OIDC authorization code: {err}"); + "Code exchange failed." + })?; + + let Some(id_token) = token_response.id_token() else { + error!("Identity server did not return an id token"); + return Err(Self::SERVER_MISCONFIGURED); + }; + + let claims = id_token + .claims(&machine.id_token_verifier(), &session.nonce) + .map_err(|err| { + error!("Failed to verify id token claims: {err}"); + Self::SERVER_MISCONFIGURED + })? + .to_owned(); + + info!(subject = claims.subject().as_str(), "Authenticated subject"); + + Ok(claims) + } + + #[tracing::instrument(skip(self, claims), fields(subject = claims.subject().to_string()))] + pub async fn complete_session( + &self, + claims: &Claims, + supplied_user_id: Option, + ) -> Result { + let Some(OidcClient { config, .. }) = self.client.as_ref() else { + return Err("Delegated authentication is not enabled on this server."); + }; + + // this is a truly awful hack but we really need all the claims in a map + let all_claims = serde_json::to_value(claims) + .expect("should be able to serialize claims") + .as_object() + .expect("claims should be an object") + .to_owned(); + + debug!(?all_claims); + + let subject = claims.subject().as_str(); + + let user_id = if let Ok(localpart) = self + .db + .openidsubject_localpart + .get(subject) + .await + .deserialized::() + { + UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name())) + .expect("saved localpart should be valid") + } else if config.prompt_for_localpart { + if let Some(supplied_user_id) = supplied_user_id { + supplied_user_id + } else { + return Ok(SessionCompletionStatus::NeedsUserId); + } + } else if let Some(preferred_username) = all_claims + .get(&config.preferred_username_claim) + .and_then(|claim| claim.as_str()) + { + self.services + .users + .determine_registration_user_id(Some(preferred_username.to_owned()), None, None) + .await + .map_err(|err| { + error!("Preferred username claim is not a valid localpart: {err}"); + "Your preferred username is not a valid Matrix user ID localpart. Contact \ + your homeserver's administrator." + })? + } else { + error!("Preferred username claim was not present or was not a string"); + return Err(Self::SERVER_MISCONFIGURED); + }; + + info!(?subject, ?user_id, "User {user_id} successfully authorized with OIDC"); + + // Create a shadow account for the user if necessary + let new_account_registered = match self.services.users.status(&user_id).await { + | AccountStatus::Active => { + // Do nothing, an account already exists + false + }, + | AccountStatus::NotFound => { + // Create a new shadow user + self.services + .users + .create_local_account(&user_id, None, None) + .await + .map_err(|err| { + error!("Failed to create a shadow user for {user_id}: {err}"); + Self::SERVER_MISCONFIGURED + })?; + + info!(?subject, ?user_id, "Shadow user created for {user_id}"); + true + }, + | AccountStatus::Deactivated => { + return Err("Your account has been deactivated."); + }, + }; + + self.link_user(&user_id, subject); + + // Import profile fields + if matches!(config.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin) + || (matches!( + config.profile_key_import_mode, + OidcProfileKeyImportMode::OnRegistration + ) && new_account_registered) + { + if let Some(email_claim) = &config.email_claim { + if let Some(email) = claims.email().map(|email| email.as_str()) + && let Ok(address) = Address::from_str(email) + { + if let Err(err) = self + .services + .threepid + .associate_localpart_email(user_id.localpart(), &address) + .await + { + warn!(?email_claim, ?address, "Failed to associate email address: {err}"); + } + } else { + warn!( + ?email_claim, + "Email claim was not present or was not a valid email address" + ); + } + } + + let user_id = user_id.clone(); + let subject = claims.subject().to_string(); + let profile_key_map = config.profile_key_map.clone(); + let openidsubject_currentpictureurl = self.db.openidsubject_currentpictureurl.clone(); + let users = self.services.users.clone(); + let media = self.services.media.clone(); + + let import_task = self.runtime.spawn(async move { + for (field, claim) in &profile_key_map { + let Some(value) = all_claims.get(claim).cloned() else { + warn!(?field, ?claim, "IDP provided no value for this mapped claim"); + continue; + }; + + let value = if let Some(picture_url) = value.as_str() + && field == ProfileFieldName::AvatarUrl.as_str() + && openidsubject_currentpictureurl + .get(&subject) + .await + .deserialized::() + .ok() + .is_none_or(|current_picture| current_picture != picture_url) + { + match media.download_media(picture_url).await { + | Ok((mxc, size)) => { + openidsubject_currentpictureurl.insert(&subject, picture_url); + info!(?picture_url, ?mxc, ?size, "Downloaded profile picture"); + + ProfileFieldValue::AvatarUrl(mxc) + }, + | Err(err) => { + warn!( + ?claim, + ?picture_url, + "Failed to download profile picture: {err}" + ); + continue; + }, + } + } else { + match ProfileFieldValue::new(field, value.clone()) { + | Ok(value) => value, + | Err(err) => { + warn!( + ?field, + ?claim, + ?value, + "Failed to parse claim value for profile field: {err}" + ); + continue; + }, + } + }; + + if let Err(err) = users + .set_profile_field( + &user_id, + ProfileFieldChange::Set(value), + PropagateTo::Unchanged, + ) + .await + { + warn!(?field, ?claim, "Error while setting profile field: {err}"); + } + } + + info!("Profile import complete"); + }); + + // Only wait for import to complete if this is a new account, + // so they see the correct profile information in the account panel + if new_account_registered { + let _ = import_task.await; + } + } + + Ok(SessionCompletionStatus::Complete(user_id)) + } + + pub fn link_user(&self, user_id: &UserId, subject: &str) { + self.db + .openidsubject_localpart + .insert(subject, user_id.localpart()); + } + + pub fn unlink_user(&self, subject: &str) { self.db.openidsubject_localpart.remove(subject); } +} diff --git a/src/service/rooms/state_cache/mod.rs b/src/service/rooms/state_cache/mod.rs index 46ef9a769..5c64ce04c 100644 --- a/src/service/rooms/state_cache/mod.rs +++ b/src/service/rooms/state_cache/mod.rs @@ -248,7 +248,7 @@ impl Service { ) -> impl Stream + Send + 'a { self.local_users_in_room(room_id) .filter_map(async |user_id| { - if self.services.users.is_active(&user_id).await { + if self.services.users.status(&user_id).await.is_active() { Some(user_id) } else { None diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 5249df182..28f5098b7 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -30,15 +30,8 @@ impl super::Service { ) -> Result { let membership = pdu.get_content::()?; - // Keep track what remote users exist by adding them as "deactivated" users - // - // TODO: use futures to update remote profiles without blocking the membership - // update - #[allow(clippy::collapsible_if)] - if !self.services.globals.user_is_local(user_id) - && !self.services.users.exists(user_id).await - { - self.services.users.create(user_id, None)?; + if !self.services.globals.user_is_local(user_id) { + self.services.users.record_remote_user(user_id); } match &membership.membership { diff --git a/src/service/rooms/timeline/append.rs b/src/service/rooms/timeline/append.rs index ab657d4fe..132ee4f0a 100644 --- a/src/service/rooms/timeline/append.rs +++ b/src/service/rooms/timeline/append.rs @@ -255,7 +255,13 @@ impl super::Service { if let Some(state_key) = pdu.state_key() { match UserId::parse(state_key) { | Ok(target_user_id) => { - if self.services.users.is_active_local(&target_user_id).await { + if self + .services + .users + .status(&target_user_id) + .await + .is_active() + { push_targets.insert(target_user_id.clone()); } }, diff --git a/src/service/services.rs b/src/service/services.rs index 0cfec43bd..92d4a899e 100644 --- a/src/service/services.rs +++ b/src/service/services.rs @@ -11,8 +11,8 @@ use crate::{ account_data, admin, announcements, antispam, appservice, client, config, emergency, federation, firstrun, globals, key_backups, mailer, manager::Manager, - media, moderation, oauth, presence, pusher, registration_tokens, resolver, rooms, sending, - server_keys, + media, moderation, oauth, oidc, presence, pusher, registration_tokens, resolver, rooms, + sending, server_keys, service::{self, Args, Map, Service}, sync, threepid, transactions, uiaa, users, }; @@ -28,6 +28,7 @@ pub struct Services { pub key_backups: Arc, pub media: Arc, pub oauth: Arc, + pub oidc: Arc, pub mailer: Arc, pub presence: Arc, pub pusher: Arc, @@ -85,6 +86,7 @@ impl Services { key_backups: build!(key_backups::Service), media: build!(media::Service), oauth: build!(oauth::Service), + oidc: build!(oidc::Service), mailer: build!(mailer::Service), presence: build!(presence::Service), pusher: build!(pusher::Service), diff --git a/src/service/threepid/mod.rs b/src/service/threepid/mod.rs index 5fbebab31..7913980da 100644 --- a/src/service/threepid/mod.rs +++ b/src/service/threepid/mod.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::HashMap, sync::Arc}; -use conduwuit::{Err, Error, Result, result::FlatOk}; +use conduwuit::{Err, Error, Result, config::OidcProfileKeyImportMode, result::FlatOk}; use database::{Deserialized, Map}; use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter}; use lettre::{Address, message::Mailbox}; @@ -33,7 +33,9 @@ pub enum EmailRequirement { Required, /// Users may change or remove their email. Optional, - /// Users may not change their email at all. + /// Users may see their email, but may not change it. + Locked, + /// Email features are disabled. Unavailable, } @@ -43,6 +45,9 @@ impl EmailRequirement { #[must_use] pub fn may_remove(&self) -> bool { matches!(self, Self::Optional) } + + #[must_use] + pub fn may_view(&self) -> bool { !matches!(self, Self::Unavailable) } } struct Data { @@ -87,7 +92,14 @@ impl Service { /// Check if users are required to have an email address. pub fn email_requirement(&self) -> EmailRequirement { if let Some(smtp) = &self.services.config.smtp { - if smtp.require_email_for_registration || smtp.require_email_for_token_registration { + if let Some(oidc) = &self.services.config.oauth.oidc + && matches!(oidc.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin) + && oidc.email_claim.is_some() + { + EmailRequirement::Locked + } else if smtp.require_email_for_registration + || smtp.require_email_for_token_registration + { EmailRequirement::Required } else { EmailRequirement::Optional diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index 2bb70c152..57665f5d6 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -314,7 +314,7 @@ impl Service { .services .config .oauth - .compatibility_mode + .compatibility_mode() .uiaa_available() { return Err!(Request(Unrecognized( diff --git a/src/service/users/account.rs b/src/service/users/account.rs index d663e7940..a12387780 100644 --- a/src/service/users/account.rs +++ b/src/service/users/account.rs @@ -6,21 +6,23 @@ use conduwuit::{ warn, }; use database::{Deserialized, Json}; -use futures::{FutureExt, Stream, StreamExt, TryFutureExt}; +use futures::{FutureExt, Stream, StreamExt}; use lettre::Address; use ruma::{ MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId, + api::client::profile::PropagateTo, events::{ GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent, push_rules::PushRulesEvent, room::message::RoomMessageEventContent, }, + profile::ProfileFieldValue, push::Ruleset, }; use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigEvent}; use crate::{ appservice::RegistrationInfo, - users::{HashedPassword, UserSuspension}, + users::{HashedPassword, UserSuspension, profile::ProfileFieldChange}, }; /// The status of an access token. @@ -29,6 +31,30 @@ pub enum AccessTokenStatus { Expired, } +/// The status of a user account. +#[derive(Clone, Copy)] +pub enum AccountStatus { + NotFound, + Active, + Deactivated, +} + +impl AccountStatus { + #[must_use] + pub fn is_found(&self) -> bool { !matches!(self, Self::NotFound) } + + #[must_use] + pub fn is_active(&self) -> bool { matches!(self, Self::Active) } + + pub fn ensure_active(&self) -> Result<()> { + match self { + | Self::Active => Ok(()), + | Self::Deactivated => Err!(Request(UserDeactivated("This account is deactivated."))), + | Self::NotFound => Err!(Request(NotFound("This account does not exist."))), + } + } +} + impl super::Service { /// Returns true/false based on whether the recipient/receiving user has /// ignored the sender. @@ -89,29 +115,33 @@ impl super::Service { self.services.admin.user_is_admin(user_id).await } - /// Create a new user account on this homeserver. Set the password to `None` - /// to create a non-local user. Non-local users with a password will return - /// an error. - #[inline] - pub fn create(&self, user_id: &UserId, password: Option) -> Result<()> { - if !self.services.globals.user_is_local(user_id) && password.is_some() { - return Err!("Cannot create a nonlocal user with a set password"); + /// Create a new shadow user account on this server. Shadow accounts + /// have no password and cannot be logged into. + pub async fn create_shadow_account(&self, user_id: &UserId) -> Result<()> { + assert!(self.services.globals.user_is_local(user_id), "user id must be local"); + + if self.status(user_id).await.is_found() { + return Err!(Request(UserInUse("An account with this user ID already exists."))); } - self.set_password(user_id, password); + self.db.userid_password.insert(user_id, ""); Ok(()) } - /// Create a new account for a local human or bot user. + /// Create a new account for a local human or bot user. If `password` is + /// None, the account will be a shadow account. pub async fn create_local_account( &self, user_id: &UserId, - password: HashedPassword, + password: Option, email: Option
, - ) { - self.create(user_id, Some(password)) - .expect("should be able to save a new local user. what happened?"); + ) -> Result<()> { + self.create_shadow_account(user_id).await?; + + if let Some(password) = password { + self.convert_to_local_account(user_id, password).await?; + } // Set an initial display name { @@ -123,7 +153,13 @@ impl super::Service { displayname.push_str(suffix); } - self.set_displayname(user_id, Some(displayname)); + self.set_profile_field( + user_id, + ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)), + PropagateTo::None, + ) + .await + .expect("should be able to set display name"); }; // Set default push rules @@ -230,6 +266,8 @@ impl super::Service { } info!("Created new user account for {user_id}"); + + Ok(()) } pub async fn determine_registration_user_id( @@ -268,28 +306,7 @@ impl super::Service { &supplied_username, self.services.globals.server_name(), ) { - | Ok(user_id) => { - if let Err(e) = user_id.validate_strict() { - // Unless we are in emergency mode, we should follow synapse's behaviour - // on not allowing things like spaces and UTF-8 characters in - // usernames - if !emergency_mode_enabled { - return Err!(Request(InvalidUsername(debug_warn!( - "Username {supplied_username} contains disallowed characters or \ - spaces: {e}" - )))); - } - } - - // Don't allow registration with user IDs that aren't local - if !self.services.globals.user_is_local(&user_id) { - return Err!(Request(InvalidUsername( - "Username {supplied_username} is not local to this server" - ))); - } - - user_id - }, + | Ok(user_id) => user_id, | Err(e) => { return Err!(Request(InvalidUsername(debug_warn!( "Username {supplied_username} is not valid: {e}" @@ -297,8 +314,27 @@ impl super::Service { }, }; - if self.exists(&user_id).await { - return Err!(Request(UserInUse("User ID is not available."))); + if let Err(e) = user_id.validate_strict() { + // Unless we are in emergency mode, we should follow synapse's behaviour + // on not allowing things like spaces and UTF-8 characters in + // usernames + if !emergency_mode_enabled { + return Err!(Request(InvalidUsername(debug_warn!( + "Username {supplied_username} contains disallowed characters or spaces: \ + {e}" + )))); + } + } + + // Don't allow registration with user IDs that aren't local + if !self.services.globals.user_is_local(&user_id) { + return Err!(Request(InvalidUsername( + "Username {supplied_username} is not local to this server" + ))); + } + + if self.status(&user_id).await.is_found() { + return Err!(Request(UserInUse("Username is not available."))); } // Check that the user ID is/is not in an appservice's namespace @@ -329,26 +365,24 @@ impl super::Service { ) .unwrap(); - if !self.exists(&user_id).await { + if !self.status(&user_id).await.is_found() { break Ok(user_id); } } } } - /// Deactivates an account, removing all of their device IDs and unsetting - /// their password. + /// Deactivates an account, removing all of their device IDs. pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> { + self.status(user_id).await.ensure_active()?; + // Remove all associated devices self.all_device_ids(user_id) .for_each(async |device_id| self.remove_device(user_id, &device_id).await) .await; - // Set the password to "" to indicate a deactivated account. Hashes will never - // 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); + // Mark user as deactivated + self.db.userid_deactivated.insert(user_id, ""); // TODO: Unhook 3PID Ok(()) @@ -393,25 +427,6 @@ impl super::Service { /// Unlocks an account, allowing the user to log in and use it again. pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); } - /// Check if the provided user ID belongs to an existing (possibly - /// deactivated) account on this homeserver. - #[inline] - pub async fn exists(&self, user_id: &UserId) -> bool { - self.services.globals.user_is_local(user_id) - && self.db.userid_password.get(user_id).await.is_ok() - } - - /// Check if account is deactivated (has an empty password). Returns a - /// NotFound error if the user does not exist. - pub async fn is_deactivated(&self, user_id: &UserId) -> Result { - self.db - .userid_password - .get(user_id) - .map_ok(|val| val.is_empty()) - .map_err(|_| err!(Request(NotFound("User does not exist.")))) - .await - } - /// Check if account is suspended. Returns false if the user does not exist. pub async fn is_suspended(&self, user_id: &UserId) -> Result { match self @@ -469,18 +484,33 @@ impl super::Service { .is_ok() } - /// Check if account is active (not deactivated) - pub async fn is_active(&self, user_id: &UserId) -> bool { - !self.is_deactivated(user_id).await.unwrap_or(true) + /// Checks the activation status of the provided user ID's account. + pub async fn status(&self, user_id: &UserId) -> AccountStatus { + if !self.services.globals.user_is_local(user_id) { + AccountStatus::NotFound + } else if self.db.userid_password.exists(user_id).await.is_ok() { + if self.db.userid_deactivated.exists(user_id).await.is_ok() { + AccountStatus::Deactivated + } else { + AccountStatus::Active + } + } else { + AccountStatus::NotFound + } } - /// Check if account is a local user, and is active (not deactivated) - pub async fn is_active_local(&self, user_id: &UserId) -> bool { - self.services.globals.user_is_local(user_id) && self.is_active(user_id).await + /// Checks if a user is a shadow. + pub async fn is_shadow(&self, user_id: &UserId) -> bool { + self.db + .userid_password + .get(user_id) + .await + .deserialized::() + .is_ok_and(|hash| hash.is_empty()) } /// Returns the number of users registered on this server, including - /// deactivated users. + /// deactivated and shadow users. #[inline] pub async fn count(&self) -> usize { self.db.userid_password.count().await } @@ -522,8 +552,8 @@ impl super::Service { Some((user_id, device_id, AccessTokenStatus::Valid)) } - /// Returns an iterator over all users on this homeserver. - pub fn stream(&self) -> impl Stream + Send { + /// Returns an iterator over all local users on this homeserver. + pub fn stream_local_users(&self) -> impl Stream + Send { self.db.userid_password.keys().ignore_err() } @@ -540,16 +570,49 @@ impl super::Service { } /// Set a user's password. - pub fn set_password(&self, user_id: &UserId, password: Option) { - if let Some(hash) = password { - self.db.userid_password.insert(user_id, hash.0); - } else { - self.db.userid_password.insert(user_id, b""); + pub async fn set_password(&self, user_id: &UserId, password: HashedPassword) -> Result { + self.status(user_id).await.ensure_active()?; + + self.db.userid_password.insert(user_id, password.0); + + Ok(()) + } + + /// Convert an existing user to an active local account. This will turn + /// shadow accounts into normal accounts and will clear the deactivation + /// flag on the user if it is set. + pub async fn convert_to_local_account( + &self, + user_id: &UserId, + password: HashedPassword, + ) -> Result { + if !self.status(user_id).await.is_found() { + return Err!(Request(NotFound("This account does not exist."))); } + + self.db.userid_deactivated.remove(user_id); + self.db.userid_password.insert(user_id, password.0); + + Ok(()) + } + + /// Convert an existing user to a shadow account. This will clear their + /// password and reactivate them if they are deactivated. + pub async fn convert_to_shadow_account(&self, user_id: &UserId) -> Result { + if !self.status(user_id).await.is_found() { + return Err!(Request(NotFound("This account does not exist."))); + } + + self.db.userid_deactivated.remove(user_id); + self.db.userid_password.insert(user_id, ""); + + Ok(()) } /// Check a user's password. pub async fn check_password(&self, user_id: &UserId, password: &str) -> Result { + self.status(user_id).await.ensure_active()?; + let (hash, user_id): (String, OwnedUserId) = if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() { (hash, user_id.to_owned()) @@ -558,21 +621,20 @@ impl super::Service { // better let lowercase_user_id = UserId::parse(user_id.as_str().to_lowercase()).unwrap(); - if let Ok(hash) = self + let hash = self .db .userid_password .get(lowercase_user_id.as_str()) .await .deserialized() - { - (hash, lowercase_user_id) - } else { - return Err!(Request(Forbidden("This user cannot log in with a password."))); - } + .expect("user should exist"); + + (hash, lowercase_user_id) }; if hash.is_empty() { - return Err!(Request(UserDeactivated("This user is deactivated"))); + // Cannot log into shadow users + return Err!(Request(Forbidden("This user cannot log in with a password."))); } utils::hash::verify_password(password, &hash) diff --git a/src/service/users/dehydrated_device.rs b/src/service/users/dehydrated_device.rs index 1711e64ed..6d536f5af 100644 --- a/src/service/users/dehydrated_device.rs +++ b/src/service/users/dehydrated_device.rs @@ -21,10 +21,7 @@ pub struct DehydratedDevice { impl super::Service { /// Creates or recreates the user's dehydrated device. pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> Result { - assert!( - self.exists(user_id).await, - "Tried to create dehydrated device for non-existent user" - ); + self.status(user_id).await.ensure_active()?; let existing_id = self.get_dehydrated_device_id(user_id).await; diff --git a/src/service/users/device.rs b/src/service/users/device.rs index bc704f1d8..9d436b152 100644 --- a/src/service/users/device.rs +++ b/src/service/users/device.rs @@ -4,8 +4,8 @@ use std::{ }; use conduwuit::{ - Err, utils, - utils::{ReadyExt, stream::TryIgnore}, + Err, Result, + utils::{self, ReadyExt, stream::TryIgnore}, }; use database::{Deserialized, Ignore, Interfix, Json}; use futures::{Stream, StreamExt}; @@ -18,8 +18,7 @@ use serde_json::json; use crate::users::increment; impl super::Service { - /// Adds a new device to a user. The user must exist, otherwise InvalidParam - /// is returned. + /// Adds a new device to a user. pub async fn create_device( &self, user_id: &UserId, @@ -28,12 +27,8 @@ impl super::Service { token_max_age: Option, initial_device_display_name: Option, client_ip: Option, - ) -> conduwuit::Result<()> { - if !self.exists(user_id).await { - return Err!(Request(InvalidParam(error!( - "Called create_device for non-existent user {user_id}" - )))); - } + ) -> Result<()> { + self.status(user_id).await.ensure_active()?; let key = (user_id, device_id); let mut device = Device::new(device_id.into()); @@ -50,7 +45,7 @@ impl super::Service { /// Removes a device from a user. pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) { // Remove dehydrated device if this is the dehydrated device - let _: conduwuit::Result<_> = self + let _ = self .remove_dehydrated_device(user_id, Some(device_id)) .await; @@ -97,11 +92,7 @@ impl super::Service { } /// Gets the access token associated with a device. - pub async fn get_token( - &self, - user_id: &UserId, - device_id: &DeviceId, - ) -> conduwuit::Result { + pub async fn get_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result { let key = (user_id, device_id); self.db.userdeviceid_token.qry(&key).await.deserialized() } @@ -131,7 +122,7 @@ impl super::Service { device_id: &DeviceId, token: &str, token_max_age: Option, - ) -> conduwuit::Result<()> { + ) -> Result<()> { let key = (user_id, device_id); if self.db.userdeviceid_metadata.qry(&key).await.is_err() { return Err!(Database(error!( @@ -259,7 +250,7 @@ impl super::Service { user_id: &UserId, device_id: &DeviceId, device: &Device, - ) -> conduwuit::Result<()> { + ) -> Result<()> { increment(&self.db.userid_devicelistversion, user_id.as_bytes()); self.update_device_metadata_no_increment(user_id, device_id, device) } @@ -273,7 +264,7 @@ impl super::Service { user_id: &UserId, device_id: &DeviceId, device: &Device, - ) -> conduwuit::Result<()> { + ) -> Result<()> { let key = (user_id, device_id); self.db.userdeviceid_metadata.put(key, Json(device)); @@ -312,7 +303,7 @@ impl super::Service { &self, user_id: &UserId, device_id: &DeviceId, - ) -> conduwuit::Result { + ) -> Result { self.db .userdeviceid_metadata .qry(&(user_id, device_id)) @@ -321,7 +312,7 @@ impl super::Service { } /// Gets the most recent device list version for a user. - pub async fn get_devicelist_version(&self, user_id: &UserId) -> conduwuit::Result { + pub async fn get_devicelist_version(&self, user_id: &UserId) -> Result { self.db .userid_devicelistversion .get(user_id) diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index ae4d47ef9..155cdeed4 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -4,20 +4,22 @@ pub(super) mod device; pub(super) mod filters; pub(super) mod keys; pub(super) mod profile; +pub(super) mod remote; use std::{mem, sync::Arc}; -pub use account::AccessTokenStatus; +pub use account::{AccessTokenStatus, AccountStatus}; use conduwuit::{ Err, Error, Result, err, utils::{self}, }; use database::Map; +pub use profile::ProfileFieldChange; use ruma::{UserId, api::error::ErrorKind, encryption::CrossSigningKey, serde::Raw}; use serde::{Deserialize, Serialize}; use crate::{ - Dep, account_data, admin, appservice, config, firstrun, globals, oauth, + Dep, account_data, admin, appservice, config, firstrun, globals, oauth, presence, rooms::{self, alias, membership}, threepid, }; @@ -61,9 +63,12 @@ struct Services { globals: Dep, membership: Dep, oauth: Dep, + presence: Dep, + state: Dep, state_accessor: Dep, state_cache: Dep, threepid: Dep, + timeline: Dep, } struct Data { @@ -75,11 +80,13 @@ struct Data { logintoken_expiresatuserid: Arc, todeviceid_events: Arc, token_userdeviceid: Arc, + remoteuserid_remoteuser: Arc, userdeviceid_tokenexpires: Arc, userdeviceid_metadata: Arc, userdeviceid_token: Arc, userfilterid_filter: Arc, userid_avatarurl: Arc, + userid_deactivated: Arc, userid_dehydrateddevice: Arc, userid_devicelistversion: Arc, userid_displayname: Arc, @@ -107,10 +114,13 @@ impl crate::Service for Service { globals: args.depend::("globals"), membership: args.depend::("membership"), oauth: args.depend::("oauth"), + presence: args.depend::("presence"), + state: args.depend::("rooms::state"), state_accessor: args .depend::("rooms::state_accessor"), state_cache: args.depend::("rooms::state_cache"), threepid: args.depend::("threepid"), + timeline: args.depend::("rooms::timeline"), }, db: Data { keychangeid_userid: args.db["keychangeid_userid"].clone(), @@ -121,10 +131,12 @@ impl crate::Service for Service { logintoken_expiresatuserid: args.db["logintoken_expiresatuserid"].clone(), todeviceid_events: args.db["todeviceid_events"].clone(), token_userdeviceid: args.db["token_userdeviceid"].clone(), + remoteuserid_remoteuser: args.db["remoteuserid_remoteuser"].clone(), userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(), userdeviceid_token: args.db["userdeviceid_token"].clone(), userfilterid_filter: args.db["userfilterid_filter"].clone(), userid_avatarurl: args.db["userid_avatarurl"].clone(), + userid_deactivated: args.db["userid_deactivated"].clone(), userid_dehydrateddevice: args.db["userid_dehydrateddevice"].clone(), userid_devicelistversion: args.db["userid_devicelistversion"].clone(), userid_displayname: args.db["userid_displayname"].clone(), diff --git a/src/service/users/profile.rs b/src/service/users/profile.rs index a4ed3ee3d..2ea869312 100644 --- a/src/service/users/profile.rs +++ b/src/service/users/profile.rs @@ -1,17 +1,276 @@ -use conduwuit::utils::{ReadyExt, stream::TryIgnore}; +use std::{borrow::Cow, collections::BTreeMap}; + +use conduwuit::{ + Err, Result, + pdu::PartialPdu, + utils::{ReadyExt, stream::TryIgnore, to_canonical_object}, +}; use database::{Deserialized, Ignore, Interfix, Json}; use futures::{Stream, StreamExt}; -use ruma::{OwnedMxcUri, UserId}; +use ruma::{ + OwnedMxcUri, UserId, + api::client::profile::PropagateTo, + events::room::member::MembershipState, + presence::PresenceState, + profile::{ProfileFieldName, ProfileFieldValue}, +}; +use serde_json::{Value, to_value}; + +pub enum ProfileFieldChange { + Set(ProfileFieldValue), + Delete(ProfileFieldName), +} + +impl ProfileFieldChange { + fn field_name(&self) -> ProfileFieldName { + match self { + | &Self::Delete(ref name) => name.clone(), + | &Self::Set(ref value) => value.field_name(), + } + } + + fn value(&self) -> Option> { + if let Self::Set(value) = self { + Some(value.value()) + } else { + None + } + } +} impl super::Service { + pub async fn set_profile_field( + &self, + user_id: &UserId, + change: ProfileFieldChange, + propagate_to: PropagateTo, + ) -> Result<()> { + const MAX_KEY_LENGTH_BYTES: usize = 255; + const MAX_PROFILE_LENGTH_BYTES: usize = 65536; + + let field_name = change.field_name(); + + // TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE, + // M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list + // of error codes and Ruma doesn't have them. Should we return those, or is + // M_TOO_LARGE okay? + if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES { + return Err!(Request(TooLarge( + "Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length." + ))); + } + + // Serialize the entire profile as canonical JSON, including the new change, + // to check if it exceeds 64 KiB + { + let mut full_profile = self.get_local_profile(user_id).await; + + match &change { + | ProfileFieldChange::Set(value) => { + full_profile.insert( + value.field_name().as_str().to_owned(), + value.value().clone().into_owned(), + ); + }, + | ProfileFieldChange::Delete(key) => { + full_profile.remove(key.as_str()); + }, + } + + if let Ok(canonical_profile) = to_canonical_object(full_profile) { + if serde_json::to_string(&canonical_profile) + .expect("should be able to serialize to string") + .len() > MAX_PROFILE_LENGTH_BYTES + { + return Err!( + "Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in \ + length." + ); + } + } else { + return Err!(Request(BadJson("Failed to canonicalize profile."))); + } + } + + // Check if this change would be a no-op + if self + .get_local_profile_field(user_id, field_name.clone()) + .await + .is_some_and(|value| Some(value.value()) == change.value()) + { + return Ok(()); + } + + // If the user is local and changed their displayname or avatar_url, update it + // in all their joined rooms. This is done before updating their profile data + // so we can check the old value of the field if `propagate_to` is `unchanged`. + if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName) + && matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged) + && self.services.globals.user_is_local(user_id) + { + let current_displayname = self.displayname(user_id).await.ok(); + let current_avatar_url = self.avatar_url(user_id).await.ok(); + + let mut all_joined_rooms = self.services.state_cache.rooms_joined(user_id); + + while let Some(room_id) = all_joined_rooms.next().await { + // TODO: this clobbers any custom fields on the event content + let mut current_membership = self + .services + .state_accessor + .get_member(&room_id, user_id) + .await + .expect("should be able to fetch membership event for joined room"); + + assert_eq!( + current_membership.membership, + MembershipState::Join, + "user should be joined" + ); + + // If `propagate_to` is `unchanged`, and the current value of the field we're + // updating was changed from its global value in this room, skip it. + if matches!(propagate_to, PropagateTo::Unchanged) { + let field_changed_from_global = match field_name { + | ProfileFieldName::AvatarUrl => + current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(), + | ProfileFieldName::DisplayName => + current_membership.displayname.as_ref() + != current_displayname.as_ref(), + | _ => unreachable!(), + }; + + if field_changed_from_global { + continue; + } + } + + let state_lock = self.services.state.mutex.lock(room_id.as_str()).await; + + // Preserve keys in accordance with the key copying rules + current_membership.reason = None; + current_membership.join_authorized_via_users_server = None; + match &change { + | ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => { + current_membership.avatar_url = Some(avatar_url.clone()); + }, + | ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => { + current_membership.displayname = Some(displayname.clone()); + }, + | ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => { + current_membership.avatar_url = None; + }, + | ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => { + current_membership.displayname = None; + }, + | _ => unreachable!(), + } + + let _ = self + .services + .timeline + .build_and_append_pdu( + PartialPdu::state(user_id.to_string(), ¤t_membership), + user_id, + Some(&room_id), + &state_lock, + ) + .await; + } + + if self.services.config.allow_local_presence { + // Send a presence EDU to indicate the profile changed + let _ = self + .services + .presence + .ping_presence(user_id, &PresenceState::Online) + .await; + } + } + + match change { + | ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => { + self.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty())); + }, + | ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => { + self.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid())); + }, + | ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => { + self.set_displayname(user_id, None); + }, + | ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => { + self.set_avatar_url(user_id, None); + }, + | other => self.set_profile_key( + user_id, + other.field_name().as_str(), + other.value().map(Cow::into_owned), + ), + } + + Ok(()) + } + + pub async fn get_local_profile(&self, user_id: &UserId) -> BTreeMap { + let mut profile = BTreeMap::new(); + + // Get displayname and avatar_url independently because `all_profile_keys` + // doesn't include them + for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] { + let key = field.as_str().to_owned(); + + if let Some(value) = self.get_local_profile_field(user_id, field).await { + profile.insert(key, value.value().into_owned()); + } + } + + // Insert all other profile fields + let mut all_fields = self.all_profile_keys(user_id); + + while let Some((key, value)) = all_fields.next().await { + profile.insert(key, value); + } + + profile + } + + pub async fn get_local_profile_field( + &self, + user_id: &UserId, + field: ProfileFieldName, + ) -> Option { + let value = match field.clone() { + | ProfileFieldName::AvatarUrl => self + .avatar_url(user_id) + .await + .ok() + .map(to_value) + .transpose() + .expect("converting avatar url to value should succeed"), + | ProfileFieldName::DisplayName => self + .displayname(user_id) + .await + .ok() + .map(to_value) + .transpose() + .expect("converting displayname to value should succeed"), + | other => self.profile_key(user_id, other.as_str()).await.ok(), + }?; + + Some( + ProfileFieldValue::new(field.as_str(), value) + .expect("local profile field should be valid"), + ) + } + /// Returns the displayname of a user on this homeserver. - pub async fn displayname(&self, user_id: &UserId) -> conduwuit::Result { + pub async fn displayname(&self, user_id: &UserId) -> Result { self.db.userid_displayname.get(user_id).await.deserialized() } /// Sets a new displayname or removes it if displayname is None. You still /// need to notify all rooms of this change. - pub fn set_displayname(&self, user_id: &UserId, displayname: Option) { + fn set_displayname(&self, user_id: &UserId, displayname: Option) { if let Some(displayname) = displayname { self.db.userid_displayname.insert(user_id, displayname); } else { @@ -20,12 +279,12 @@ impl super::Service { } /// Get the `avatar_url` of a user. - pub async fn avatar_url(&self, user_id: &UserId) -> conduwuit::Result { + pub async fn avatar_url(&self, user_id: &UserId) -> Result { self.db.userid_avatarurl.get(user_id).await.deserialized() } /// Sets a new avatar_url or removes it if avatar_url is None. - pub fn set_avatar_url(&self, user_id: &UserId, avatar_url: Option) { + fn set_avatar_url(&self, user_id: &UserId, avatar_url: Option) { match avatar_url { | Some(avatar_url) => { self.db.userid_avatarurl.insert(user_id, &avatar_url); @@ -37,11 +296,7 @@ impl super::Service { } /// Gets a specific user profile key - pub async fn profile_key( - &self, - user_id: &UserId, - profile_key: &str, - ) -> conduwuit::Result { + pub async fn profile_key(&self, user_id: &UserId, profile_key: &str) -> Result { let key = (user_id, profile_key); self.db .useridprofilekey_value @@ -54,7 +309,7 @@ impl super::Service { pub fn all_profile_keys<'a>( &'a self, user_id: &'a UserId, - ) -> impl Stream + 'a + Send { + ) -> impl Stream + 'a + Send { type KeyVal<'a> = ((Ignore, String), &'a [u8]); let prefix = (user_id, Interfix); @@ -67,11 +322,11 @@ impl super::Service { } /// Sets a new profile key value, removes the key if value is None - pub fn set_profile_key( + fn set_profile_key( &self, user_id: &UserId, profile_key: &str, - profile_key_value: Option, + profile_key_value: Option, ) { let key = (user_id, profile_key); diff --git a/src/service/users/remote.rs b/src/service/users/remote.rs new file mode 100644 index 000000000..c2c77967c --- /dev/null +++ b/src/service/users/remote.rs @@ -0,0 +1,17 @@ +use conduwuit::utils::stream::TryIgnore; +use futures::Stream; +use ruma::{OwnedUserId, UserId}; + +impl super::Service { + /// Record the existence of a remote user. + pub fn record_remote_user(&self, user_id: &UserId) { + assert!(!self.services.globals.user_is_local(user_id), "user is not remote"); + + self.db.remoteuserid_remoteuser.insert(user_id, ""); + } + + /// Returns a stream over all remote users this server has ever seen. + pub fn stream_remote_users(&self) -> impl Stream + Send { + self.db.remoteuserid_remoteuser.keys().ignore_err() + } +} diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml index d70335ebf..516ff1712 100644 --- a/src/web/Cargo.toml +++ b/src/web/Cargo.toml @@ -49,6 +49,7 @@ url.workspace = true recaptcha-verify = { version = "0.2.0", default-features = false } reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated form_urlencoded = "1.2.2" +openidconnect.workspace = true [build-dependencies] memory-serve = "2.1.0" diff --git a/src/web/mod.rs b/src/web/mod.rs index 633b64191..847ca80fe 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -133,6 +133,7 @@ pub fn build(services: &Services) -> Router { .nest("/account/", account::build()) .merge(debug::build()) .nest("/oauth2/", oauth::build()) + .nest("/oidc/", oidc::build()) .merge(resources::build()) .merge(threepid::build()) .fallback(async || WebError::NotFound), diff --git a/src/web/pages/account/email.rs b/src/web/pages/account/email.rs index cc9b9e294..60baea17b 100644 --- a/src/web/pages/account/email.rs +++ b/src/web/pages/account/email.rs @@ -81,6 +81,10 @@ async fn route_change_email( ) -> Result { let user_id = user.expect_recent(LoginTarget::ChangeEmail)?; + if !services.threepid.email_requirement().may_change() { + return Err(WebError::Forbidden("You may not change your email address.".to_owned())); + } + let Some(form) = form else { return response!(ChangeEmail::new( context.clone(), diff --git a/src/web/pages/account/login.rs b/src/web/pages/account/login.rs index 52ef6cd69..b92e775bb 100644 --- a/src/web/pages/account/login.rs +++ b/src/web/pages/account/login.rs @@ -7,12 +7,14 @@ use axum::{ routing::{get, on}, }; use conduwuit_api::client::handle_login; +use openidconnect::core::CoreAuthPrompt; use ruma::{ OwnedUserId, api::client::uiaa::{EmailUserIdentifier, MatrixUserIdentifier, UserIdentifier}, }; use serde::Deserialize; use tower_sessions::Session; +use url::Url; use crate::{ ROUTE_PREFIX, WebError, @@ -21,9 +23,10 @@ use crate::{ GET_POST, Result, TemplateContext, account::register::{TrustedFlowStatus, UntrustedFlowStatus, registration_flow_status}, components::UserCard, + oidc::{OIDC_SESSION_ID_KEY, OidcSession, OidcSessionState}, }, response, - session::{LoginQuery, LoginTarget, User, UserSession}, + session::{LoginIntent, LoginQuery, LoginTarget, User, UserSession}, template, }; @@ -36,6 +39,7 @@ pub(crate) fn build() -> Router { template! { struct Login use "login.html.j2" { body: LoginBody, + login_type: LoginType, login_error: Option } } @@ -44,7 +48,6 @@ template! { enum LoginBody { Unauthenticated { server_name: String, - registration_available: bool, next: Option, }, Authenticated { @@ -52,6 +55,16 @@ enum LoginBody { }, } +#[derive(Debug)] +enum LoginType { + Interactive { + registration_available: bool, + }, + Oidc { + redirect_url: Url, + }, +} + #[derive(Deserialize)] struct LoginForm { identifier: Option, @@ -61,27 +74,46 @@ struct LoginForm { async fn route_login( State(services): State, Extension(context): Extension, - Expect(Query(LoginQuery { next, reauthenticate })): Expect>, + Expect(Query(LoginQuery { next, reauthenticate, intent })): Expect>, session_store: Session, user: User, PostForm(form): PostForm, ) -> Result { let user_id = user.into_session().map(|session| session.user_id); + let login_type = if services.oidc.enabled() { + let (session, redirect_url) = services + .oidc + .begin_session(match intent { + | Some(LoginIntent::SwitchAccounts) => Some(CoreAuthPrompt::SelectAccount), + | _ if reauthenticate => Some(CoreAuthPrompt::Consent), + | _ => None, + }) + .await; + + session_store + .insert(OIDC_SESSION_ID_KEY, OidcSession { + next: next.clone().unwrap_or_default(), + state: OidcSessionState::CodeExchange { expected_user: user_id.clone(), session }, + }) + .await + .expect("should be able to serialize OIDC session"); + + LoginType::Oidc { redirect_url } + } else { + let (trusted_flow_status, untrusted_flow_status) = + registration_flow_status(&services).await; + + let registration_available = matches!(trusted_flow_status, TrustedFlowStatus::Available) + || matches!(untrusted_flow_status, UntrustedFlowStatus::Available { .. }); + + LoginType::Interactive { registration_available } + }; + let body = match &user_id { - | None => { - let (trusted_flow_status, untrusted_flow_status) = - registration_flow_status(&services).await; - - let registration_available = - matches!(trusted_flow_status, TrustedFlowStatus::Available) - || matches!(untrusted_flow_status, UntrustedFlowStatus::Available { .. }); - - LoginBody::Unauthenticated { - server_name: services.globals.server_name().to_string(), - registration_available, - next: next.clone(), - } + | None => LoginBody::Unauthenticated { + server_name: services.globals.server_name().to_string(), + next: next.clone(), }, | Some(user_id) => { if !reauthenticate { @@ -94,7 +126,7 @@ async fn route_login( }, }; - let mut template = Login::new(context, body, None); + let mut template = Login::new(context, body, login_type, None); if let Some(form) = form { let login_result = match (user_id, form.identifier) { diff --git a/src/web/pages/account/mod.rs b/src/web/pages/account/mod.rs index ace945967..cf3d86d98 100644 --- a/src/web/pages/account/mod.rs +++ b/src/web/pages/account/mod.rs @@ -66,6 +66,7 @@ template! { enum AccountBody { Unlocked { suspended: bool, + oidc_enabled: bool, email_requirement: EmailRequirement, email: Option, devices: Vec, @@ -128,6 +129,7 @@ async fn get_account( response!(Account::new(context, user_card, AccountBody::Unlocked { suspended, + oidc_enabled: services.oidc.enabled(), email_requirement, email, devices: device_cards diff --git a/src/web/pages/account/password/change.rs b/src/web/pages/account/password/change.rs index f484b72c5..74a5a5f64 100644 --- a/src/web/pages/account/password/change.rs +++ b/src/web/pages/account/password/change.rs @@ -4,6 +4,7 @@ use ruma::UserId; use validator::{Validate, ValidationError, ValidationErrors}; use crate::{ + WebError, extract::PostForm, form, pages::{ @@ -65,11 +66,17 @@ async fn route_change_password( user: User, PostForm(form): PostForm, ) -> Result { + if services.oidc.enabled() { + return Err(WebError::BadRequest( + "Password changing is not available on this server".to_owned(), + )); + } + let user_id = user.expect(LoginTarget::ChangePassword)?; let user_card = UserCard::for_local_user(&services, user_id.clone()).await; let body = if let Some(form) = form { - match change_password(&services, &user_id, form).await { + match change_password(&services, &user_id, form).await? { | Ok(()) => ChangePasswordBody::Success, | Err(errors) => ChangePasswordBody::Form(ChangePasswordForm::with_errors(context.clone(), errors)), @@ -85,7 +92,7 @@ async fn change_password( services: &crate::State, user_id: &UserId, form: ChangePasswordForm, -) -> Result<(), ValidationErrors> { +) -> Result> { form.validate()?; if services @@ -100,12 +107,12 @@ async fn change_password( ValidationError::new("wrong").with_message("Incorrect password".into()), ); - return Err(errors); + return Ok(Err(errors)); } match HashedPassword::new(&form.new_password) { | Ok(hash) => { - services.users.set_password(user_id, Some(hash)); + services.users.set_password(user_id, hash).await?; }, | Err(err) => { let mut errors = ValidationErrors::new(); @@ -114,9 +121,9 @@ async fn change_password( ValidationError::new("malformed").with_message(err.message().into()), ); - return Err(errors); + return Ok(Err(errors)); }, } - Ok(()) + Ok(Ok(())) } diff --git a/src/web/pages/account/password/reset.rs b/src/web/pages/account/password/reset.rs index 7428fb4ab..b95601fc9 100644 --- a/src/web/pages/account/password/reset.rs +++ b/src/web/pages/account/password/reset.rs @@ -65,6 +65,13 @@ async fn route_reset_password( return response!(ResetPassword::new(context, ResetPasswordBody::Unavailable)); } + // Check if OIDC is enabled + if services.oidc.enabled() { + return Err(WebError::BadRequest( + "Password resets are not available on this server".to_owned(), + )); + } + let Some(form) = form else { // For GET requests return the reset request form return response!(ResetPassword::new( @@ -214,7 +221,7 @@ async fn route_reset_password_validate( | Ok(hash) => { let _ = session.consume(); - services.users.set_password(&user_id, Some(hash)); + services.users.set_password(&user_id, hash).await?; ResetPasswordValidateBody::ResetSuccess { user_card } }, diff --git a/src/web/pages/account/register.rs b/src/web/pages/account/register.rs index 529901ef4..e47a84b3d 100644 --- a/src/web/pages/account/register.rs +++ b/src/web/pages/account/register.rs @@ -134,13 +134,15 @@ async fn route_register( ) -> Result { let is_first_run = services.firstrun.is_first_run(); - if session_store - .get::(User::KEY) - .await - .unwrap() - .is_some() + if services.oidc.enabled() + || session_store + .get::(User::KEY) + .await + .unwrap() + .is_some() { - // Redirect already logged-in users to the account panel + // Redirect to the account panel if the user is already logged in + // or OIDC is enabled return response!(Redirect::to(&LoginTarget::Account.target_path())); } @@ -310,7 +312,7 @@ async fn get_register_email_validate( response!( complete_registration(&services, session_store, completed_registration, Some(email)) - .await + .await? ) } @@ -502,7 +504,7 @@ async fn begin_registration( } else { // If email isn't required we can immediately complete registration Ok(response!( - complete_registration(services, session_store, completed_registration, None).await + complete_registration(services, session_store, completed_registration, None).await? )) } } @@ -517,11 +519,11 @@ async fn complete_registration( next, }: CompletedRegistration, email: Option
, -) -> Redirect { +) -> Result { services .users - .create_local_account(&user_id, password_hash, email) - .await; + .create_local_account(&user_id, Some(password_hash), email) + .await?; if let Some(registration_token) = registration_token { services @@ -536,7 +538,7 @@ async fn complete_registration( .await .expect("should be able to serialize user session"); - Redirect::to(&next.unwrap_or_default().target_path()) + Ok(Redirect::to(&next.unwrap_or_default().target_path())) } pub(super) async fn registration_flow_status( diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs index e2e7ff8fe..c09e405d3 100644 --- a/src/web/pages/mod.rs +++ b/src/web/pages/mod.rs @@ -17,6 +17,7 @@ mod components; pub(super) mod debug; pub(super) mod index; pub(super) mod oauth; +pub(super) mod oidc; pub(super) mod resources; pub(super) mod threepid; diff --git a/src/web/pages/oauth/grant.rs b/src/web/pages/oauth/grant.rs index 1ec5dae9d..aa2825e1d 100644 --- a/src/web/pages/oauth/grant.rs +++ b/src/web/pages/oauth/grant.rs @@ -17,7 +17,7 @@ use crate::{ components::{Avatar, AvatarType, ClientScopes}, }, response, - session::{LoginQuery, LoginTarget, User}, + session::{LoginIntent, LoginQuery, LoginTarget, User}, template, }; @@ -129,7 +129,8 @@ async fn route_authorization_code( context, serde_urlencoded::to_string(LoginQuery { next: Some(LoginTarget::AuthorizationCode(query)), - reauthenticate: false, + intent: Some(LoginIntent::SwitchAccounts), + ..Default::default() }) .unwrap(), user_id, diff --git a/src/web/pages/oidc/complete.rs b/src/web/pages/oidc/complete.rs new file mode 100644 index 000000000..62314e48f --- /dev/null +++ b/src/web/pages/oidc/complete.rs @@ -0,0 +1,196 @@ +use std::time::SystemTime; + +use axum::{ + Extension, Router, + extract::{Query, State}, + response::Redirect, + routing::on, +}; +use conduwuit_service::{oauth::grant::AuthorizationCodeResponse, oidc::SessionCompletionStatus}; +use futures::FutureExt; +use ruma::{OwnedServerName, UserId}; +use serde::{Deserialize, de::IgnoredAny}; +use tower_sessions::Session; + +use crate::{ + WebError, + extract::{Expect, PostForm}, + pages::{ + GET_POST, Result, TemplateContext, + components::UserCard, + oidc::{OIDC_SESSION_ID_KEY, OidcSession, OidcSessionState}, + }, + response, + session::{User, UserSession}, + template, +}; + +pub(crate) fn build() -> Router { + Router::new().route("/", on(GET_POST, route_complete)) +} + +template! { + struct OidcComplete use "oidc_complete.html.j2" { + body: OidcCompleteBody + } +} + +#[derive(Debug)] +enum OidcCompleteBody { + UsernamePrompt { + server_name: OwnedServerName, + username_error: Option, + }, + PasswordPrompt { + username: String, + user_card: UserCard, + password_error: bool, + }, +} + +#[derive(Deserialize)] +struct LoginForm { + username: String, + password: Option, +} + +async fn route_complete( + State(services): State, + Extension(context): Extension, + Expect(Query(query)): Expect>, + session_store: Session, + user: User, + PostForm(form): PostForm, +) -> Result { + let user_id = user.into_session().map(|session| session.user_id); + + let Some(session) = session_store + .get::(OIDC_SESSION_ID_KEY) + .await + .expect("should be able to deserialize oidc session") + else { + return response!(WebError::BadRequest( + "No OIDC session found. What are you doing here?".to_owned() + )); + }; + + let session_completion_status = match session.state { + | OidcSessionState::CodeExchange { expected_user, session: pending_session } => { + if let (Some(user_id), Some(expected_user)) = (&user_id, &expected_user) + && user_id != expected_user + { + return response!(WebError::BadRequest( + "Identity mismatch. You may have switched accounts at your identity \ + provider. Please log out and back in to continue." + .to_owned() + )); + } + + let claims = services + .oidc + .exchange_code(pending_session, query) + .boxed() + .await + .map_err(|err| WebError::BadRequest(err.to_owned()))?; + + session_store + .insert(OIDC_SESSION_ID_KEY, OidcSession { + next: session.next.clone(), + state: OidcSessionState::Authorized { claims: Box::new(claims.clone()) }, + }) + .await + .expect("Should be able to serialize oidc session"); + + services.oidc.complete_session(&claims, None).await + }, + | OidcSessionState::Authorized { claims } => { + let supplied_user_id = if let Some(form) = form { + if let Ok(user_id) = UserId::parse(format!( + "@{}:{}", + &form.username, + services.globals.server_name() + )) && services.users.status(&user_id).await.is_active() + { + let user_card = UserCard::for_local_user(&services, user_id.clone()).await; + + if let Some(password) = form.password { + if services + .users + .check_password(&user_id, &password) + .await + .is_ok() + { + Some(user_id) + } else { + return response!(OidcComplete::new( + context, + OidcCompleteBody::PasswordPrompt { + username: form.username, + user_card, + password_error: true + } + )); + } + } else { + return response!(OidcComplete::new( + context, + OidcCompleteBody::PasswordPrompt { + username: form.username, + user_card, + password_error: false, + } + )); + } + } else { + match services + .users + .determine_registration_user_id(Some(form.username), None, None) + .await + { + | Ok(user_id) => Some(user_id), + | Err(err) => { + return response!(OidcComplete::new( + context, + OidcCompleteBody::UsernamePrompt { + server_name: services.globals.server_name().to_owned(), + username_error: Some(err.message()), + } + )); + }, + } + } + } else { + None + }; + + services + .oidc + .complete_session(&claims, supplied_user_id) + .await + }, + } + .map_err(|err| WebError::BadRequest(err.to_owned()))?; + + match session_completion_status { + | SessionCompletionStatus::Complete(user_id) => { + let _ = session_store + .remove::(OIDC_SESSION_ID_KEY) + .await; + + let user_session = UserSession { user_id, last_login: SystemTime::now() }; + + session_store + .insert(User::KEY, user_session) + .await + .expect("should be able to serialize user session"); + + response!(Redirect::to(&session.next.target_path())) + }, + | SessionCompletionStatus::NeedsUserId => { + response!(OidcComplete::new(context, OidcCompleteBody::UsernamePrompt { + server_name: services.globals.server_name().to_owned(), + username_error: None + })) + }, + } +} diff --git a/src/web/pages/oidc/mod.rs b/src/web/pages/oidc/mod.rs new file mode 100644 index 000000000..74e73e59b --- /dev/null +++ b/src/web/pages/oidc/mod.rs @@ -0,0 +1,34 @@ +use axum::Router; +use conduwuit_service::oidc::{self, Claims}; +use ruma::OwnedUserId; +use serde::{Deserialize, Serialize}; + +use crate::session::LoginTarget; + +mod complete; + +pub(crate) const OIDC_SESSION_ID_KEY: &str = "oidc_session"; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct OidcSession { + pub next: LoginTarget, + pub state: OidcSessionState, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum OidcSessionState { + CodeExchange { + expected_user: Option, + session: oidc::PendingSession, + }, + Authorized { + claims: Box, + }, +} + +pub(crate) fn build() -> Router { + #[allow(clippy::wildcard_imports)] + use self::*; + + Router::new().nest("/complete", complete::build()) +} diff --git a/src/web/pages/resources/common.css b/src/web/pages/resources/common.css index f3307c72d..9a03e7c59 100644 --- a/src/web/pages/resources/common.css +++ b/src/web/pages/resources/common.css @@ -341,18 +341,8 @@ ul.bullet-separated { margin-top: 0; } - ul.bullet-separated { - display: block; - padding-left: inherit; - - li { - display: list-item; - list-style-type: disc; - - &::before { - content: none !important; - } - } + .mobile-hidden { + display: none; } } diff --git a/src/web/pages/resources/login.css b/src/web/pages/resources/login.css index ab5123885..d36810727 100644 --- a/src/web/pages/resources/login.css +++ b/src/web/pages/resources/login.css @@ -33,6 +33,7 @@ padding: 0.5em; margin-bottom: 0.5em; line-height: 1; + align-items: baseline; border-radius: var(--border-radius-sm); border: 2px solid var(--secondary); diff --git a/src/web/pages/templates/_components/device_card.html.j2 b/src/web/pages/templates/_components/device_card.html.j2 index b8ecfc6b6..cc6c4f372 100644 --- a/src/web/pages/templates/_components/device_card.html.j2 +++ b/src/web/pages/templates/_components/device_card.html.j2 @@ -11,7 +11,7 @@ {% if style == DeviceCardStyle::Detailed %} - • + •
  • {{ device_id }}
  • diff --git a/src/web/pages/templates/account.html.j2 b/src/web/pages/templates/account.html.j2 index 6e73ce15b..5cee286e9 100644 --- a/src/web/pages/templates/account.html.j2 +++ b/src/web/pages/templates/account.html.j2 @@ -9,7 +9,7 @@ Your account

    Manage your account

    {{ user_card }} {% match body %} - {% when AccountBody::Unlocked { suspended, email_requirement, email, devices } %} + {% when AccountBody::Unlocked { suspended, email_requirement, email, devices, oidc_enabled } %} {% if suspended %}

    ⚠️ Your account has been suspended by your homeserver's administrator. @@ -17,19 +17,23 @@ Your account

    {% endif %}
    - {% if email_requirement.may_change() %} + {% if email_requirement.may_view() %}

    {% if let Some(email) = email %} Your account's associated email address is {{ email }}. {% else %} Your account has no associated email address. {% endif %} - Change your email + {% if email_requirement.may_change() %} + Change your email\ + {% endif %} +

    + {% endif %} + {% if !oidc_enabled %} +

    + Change your password

    {% endif %} -

    - Change your password -

    diff --git a/src/web/pages/templates/login.html.j2 b/src/web/pages/templates/login.html.j2 index a2ade2e04..9f118e0bc 100644 --- a/src/web/pages/templates/login.html.j2 +++ b/src/web/pages/templates/login.html.j2 @@ -9,9 +9,14 @@ Log in {%- endblock -%} {%- block content -%} +{% match login_type %} + {% when LoginType::Interactive { .. } %}
    + {% when LoginType::Oidc { .. } %} +
    +{% endmatch %} {% match body %} - {% when LoginBody::Unauthenticated { server_name, registration_available, next } %} + {% when LoginBody::Unauthenticated { server_name, next } %}

    {% if next.is_some() %} Log in to continue @@ -25,39 +30,49 @@ Log in

    You're about to log in to your account on {{ server_name }}

    -
    -
    -

    - - -

    -

    - - -

    - -
    - + {% match login_type %} + {% when LoginType::Interactive { registration_available } %} +
    +
    +

    + + +

    +

    + + +

    + +
    + + {% when LoginType::Oidc { redirect_url } %} + Continue + {% endmatch %} {% when LoginBody::Authenticated { user_card } %}

    Confirm your identity

    {{ user_card }} -

    Enter your password to continue.

    -
    -

    - - -

    - -
    - + {% match login_type %} + {% when LoginType::Interactive { .. } %} +

    Enter your password to continue.

    +
    +

    + + +

    + +
    + + {% when LoginType::Oidc { redirect_url } %} + Continue + {% endmatch %} {% endmatch %} {% if let Some(error) = login_error %} {{ error }} diff --git a/src/web/pages/templates/oidc_complete.html.j2 b/src/web/pages/templates/oidc_complete.html.j2 new file mode 100644 index 000000000..be58288ad --- /dev/null +++ b/src/web/pages/templates/oidc_complete.html.j2 @@ -0,0 +1,59 @@ +{% extends "_layout.html.j2" %} +{% import "_components/form.html.j2" as form %} + +{%- block head -%} + +{%- endblock -%} + +{%- block title -%} +Link your account +{%- endblock -%} + +{%- block content -%} +
    +

    + Link your account + + Matrix logo + +

    + {% match body %} + {% when OidcCompleteBody::UsernamePrompt { server_name, username_error } %} +
    +

    + To finish linking your account to Matrix, choose a username. +
    If you have an existing Matrix account, enter its user ID to link it. +

    +

    + + + @ + + :{{ server_name }} + + {% if let Some(username_error) = username_error %} + + {{ username_error }} + + {% endif %} + Your username cannot be changed after you link your account. +

    + +
    + {% when OidcCompleteBody::PasswordPrompt { username, user_card, password_error } %} + {{ user_card }} +
    +

    To link this legacy account, enter the password you used to use when logging in.

    +

    + + + {% if password_error %} + Incorrect password + {% endif %} +

    + + +
    + {% endmatch %} +
    +{% endblock %} diff --git a/src/web/session/mod.rs b/src/web/session/mod.rs index ccc1e6b27..526915243 100644 --- a/src/web/session/mod.rs +++ b/src/web/session/mod.rs @@ -24,6 +24,8 @@ pub(crate) struct LoginQuery { pub next: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub reauthenticate: bool, + #[serde(default)] + pub intent: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -65,6 +67,12 @@ impl LoginTarget { } } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum LoginIntent { + SwitchAccounts, +} + /// An extractor that fetches the authenticated user. pub(crate) struct User(Option); @@ -107,7 +115,7 @@ impl User { } else { Err(WebError::LoginRequired(LoginQuery { next: Some(or_else), - reauthenticate: false, + ..Default::default() })) } } @@ -122,12 +130,13 @@ impl User { Err(WebError::LoginRequired(LoginQuery { next: Some(or_else), reauthenticate: true, + ..Default::default() })) } } else { Err(WebError::LoginRequired(LoginQuery { next: Some(or_else), - reauthenticate: false, + ..Default::default() })) } } @@ -162,10 +171,8 @@ pub(crate) async fn require_active( user_id: &UserId, allow_locked: bool, ) -> Result<(), Response> { - if !services.users.is_active(user_id).await { - return Err( - WebError::Forbidden("Your account is deactivated.".to_owned()).into_response() - ); + if let Err(err) = services.users.status(user_id).await.ensure_active() { + return Err(WebError::Forbidden(err.message()).into_response()); } if !allow_locked