diff --git a/Cargo.lock b/Cargo.lock index 68d885dda..21ef360fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,7 +385,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -417,7 +417,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.0", "futures-lite", - "rustix", + "rustix 0.38.44", "tracing", ] @@ -433,7 +433,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -1188,33 +1188,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4b56ebe316895d3fa37775d0a87b0c889cc933f5c8b253dbcc7c7bcb7fe7e4" +checksum = "2ce81edaca6167d1f78da026afa92d7ff957a80aa82a79076e11cd34cde20165" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cabbc01dfbd7dcd6c329ca44f0212910309c221797ac736a67a5bc8857fe1b" +checksum = "4d0d51e12f958551165969c6e8767e1e461729f6c1ccae923b0ba1d5cbcbbbf8" +dependencies = [ + "cranelift-srcgen", +] [[package]] name = "cranelift-bforest" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ffe46df300a45f1dc6f609dc808ce963f0e3a2e971682c479a2d13e3b9b8ef" +checksum = "41294c755094d2c8a514cea903039742474423f2e91601332eab5f4094f76333" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b265bed7c51e1921fdae6419791d31af77d33662ee56d7b0fa0704dc8d231cab" +checksum = "ebb6f5d0df5bd0d02c63ec48e8f2e38a176b123f59e084f22caf89a0d0593e7e" dependencies = [ "serde", "serde_derive", @@ -1222,9 +1225,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e606230a7e3a6897d603761baee0d19f88d077f17b996bb5089488a29ae96e41" +checksum = "e543cdb278b7c15f739021cf880ee1808c68fa2402febb87edb9307f552c8fec" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1244,39 +1247,41 @@ dependencies = [ "serde", "smallvec", "target-lexicon", + "wasmtime-math", ] [[package]] name = "cranelift-codegen-meta" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a63bffafc23bc60969ad528e138788495999d935f0adcfd6543cb151ca8637d" +checksum = "f979c75cfd712dbc754799dfe4a4d0db7a51defc2e36d006b27a8a63e018eece" dependencies = [ - "cranelift-assembler-x64", + "cranelift-assembler-x64-meta", "cranelift-codegen-shared", + "cranelift-srcgen", "pulley-interpreter", ] [[package]] name = "cranelift-codegen-shared" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af50281b67324b58e843170a6a5943cf6d387c06f7eeacc9f5696e4ab7ae7d7e" +checksum = "d2f36e74ba4033490587a47952f74390cb7d4f1fc1fa28ace50564e491f1e38f" [[package]] name = "cranelift-control" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c20c1b38d1abfbcebb0032e497e71156c0e3b8dcb3f0a92b9863b7bcaec290c" +checksum = "f6671962c7d65b9a7ad038cd92da6784744d8a9ecf8ded8bb9a1f7046dbe2ccf" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2c67d95507c51b4a1ff3f3555fe4bfec36b9e13c1b684ccc602736f5d5f4a2" +checksum = "ee832f8329fa87c5df6c1d64a8506a58031e6f8a190d9b21b1900272a4dbb47d" dependencies = [ "cranelift-bitset", "serde", @@ -1285,9 +1290,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e002691cc69c38b54fc7ec93e5be5b744f627d027031d991cc845d1d512d0ce" +checksum = "4f7bc17aa3277214eab4b63a03544b1b46962154012b751c9f14c2a5419c6471" dependencies = [ "cranelift-codegen", "log", @@ -1297,21 +1302,27 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93588ed1796cbcb0e2ad160403509e2c5d330d80dd6e0014ac6774c7ebac496" +checksum = "cff02dcecae2e7e9c61b713f1fb46eabecdca9f55b49f99859ceb1a3e7f4a9cb" [[package]] name = "cranelift-native" -version = "0.118.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b09bdd6407bf5d89661b80cf926ce731c9e8cc184bf49102267a2369a8358e" +checksum = "90f76fd681f35bdf17be9c3e516b9acc0c7bd61b81faf95496decd8e0000979c" dependencies = [ "cranelift-codegen", "libc", "target-lexicon", ] +[[package]] +name = "cranelift-srcgen" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3d9071bc5ee5573e723d9d84a45b7025a29e8f2c5ad81b3b9d0293129541d9" + [[package]] name = "crc" version = "3.3.0" @@ -1746,7 +1757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3046,7 +3057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -3071,6 +3082,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "listenfd" version = "1.0.2" @@ -3816,7 +3833,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix", + "rustix 0.38.44", ] [[package]] @@ -4075,9 +4092,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opa-wasm" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c07ec35ceaacb13349669e772705036975bde72b612e72b26a6bd6a71d909" +checksum = "ffac78b55eda1af0a1fb2360b636ca8e6d23ee62865f0a24148a6d51e2f70088" dependencies = [ "anyhow", "base64", @@ -4354,12 +4371,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -4590,7 +4601,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -4785,15 +4796,27 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3325791708ad50580aeacfcce06cb5e462c9ba7a2368e109cb2012b944b70e" +checksum = "be14280b69a9cbb6ada02a7aa5f7b3f1b72d1043b5bc9336990b700525dea6e3" dependencies = [ "cranelift-bitset", "log", + "pulley-macros", "wasmtime-math", ] +[[package]] +name = "pulley-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f1be746801280af4c96c4407b5fd1d09cfa53ab27ba0ac7dd8f207e7bbf83" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quanta" version = "0.12.6" @@ -4860,7 +4883,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5004,9 +5027,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.11.2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" dependencies = [ "allocator-api2", "bumpalo", @@ -5245,8 +5268,21 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.59.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.52.0", ] [[package]] @@ -5314,7 +5350,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5934,12 +5970,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "sqlx" version = "0.8.6" @@ -6156,7 +6186,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6291,16 +6321,15 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.15.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.2.15", + "getrandom 0.3.2", "once_cell", - "rustix", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.52.0", ] [[package]] @@ -7101,9 +7130,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.226.0" +version = "0.233.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d81b727619aec227dce83e7f7420d4e56c79acd044642a356ea045b98d4e13" +checksum = "9679ae3cf7cfa2ca3a327f7fab97f27f3294d402fd1a76ca8ab514e17973e4d3" dependencies = [ "leb128fmt", "wasmparser", @@ -7111,9 +7140,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.226.0" +version = "0.233.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc28600dcb2ba68d7e5f1c3ba4195c2bddc918c0243fd702d0b6dbd05689b681" +checksum = "b51cb03afce7964bbfce46602d6cb358726f36430b6ba084ac6020d8ce5bc102" dependencies = [ "bitflags", "hashbrown 0.15.2", @@ -7124,9 +7153,9 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.226.0" +version = "0.233.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "753a0516fa6c01756ee861f36878dfd9875f273aea9409d9ea390a333c5bcdc2" +checksum = "abf8e5b732895c99b21aa615f1b73352e51bbe2b2cb6c87eae7f990d07c1ac18" dependencies = [ "anyhow", "termcolor", @@ -7135,9 +7164,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fe78033c72da8741e724d763daf1375c93a38bfcea99c873ee4415f6098c3f" +checksum = "ec10e50038f22ab407fdd8708120b8feed3450a02618efcf26ca47e82122927d" dependencies = [ "addr2line", "anyhow", @@ -7154,16 +7183,14 @@ dependencies = [ "memfd", "object", "once_cell", - "paste", "postcard", "psm", "pulley-interpreter", "rayon", - "rustix", + "rustix 1.0.8", "serde", "serde_derive", "smallvec", - "sptr", "target-lexicon", "trait-variant", "wasmparser", @@ -7181,18 +7208,18 @@ dependencies = [ [[package]] name = "wasmtime-asm-macros" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f3d44ae977d70ccf80938b371d5ec60b6adedf60800b9e8dd1223bb69f4cbc" +checksum = "4d379cda46d6fd18619e282a75fbb09b70b3d0f166b605f45b4059dfaf9dc6ce" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-component-macro" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397e68ee29eb072d8d8741c9d2c971a284cd1bc960ebf2c1f6a33ea6ba16d6e1" +checksum = "6b08be093e0a876da45f79070c2ada4656f2785eb77c01b86ce60be3153920a5" dependencies = [ "anyhow", "proc-macro2", @@ -7205,15 +7232,15 @@ dependencies = [ [[package]] name = "wasmtime-component-util" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f292ef5eb2cf3d414c2bde59c7fa0feeba799c8db9a8c5a656ad1d1a1d05e10b" +checksum = "f0451ce0dd94a33d0dbd57934ce666a04c2753a5262ca2bc84cf6a67cf5303dc" [[package]] name = "wasmtime-cranelift" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fc12eb8ea695a30007a4849a5fd56209dd86a15579e92e0c27c27122818505" +checksum = "15aa836683d7398f13f2f26bbe74c404ceaba66b6bbb96700d6b7f91bec90e03" dependencies = [ "anyhow", "cfg-if", @@ -7223,23 +7250,24 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "gimli", - "itertools 0.12.1", + "itertools 0.14.0", "log", "object", "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 1.0.69", + "thiserror 2.0.12", "wasmparser", "wasmtime-environ", + "wasmtime-math", "wasmtime-versioned-export-macros", ] [[package]] name = "wasmtime-environ" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6b4bf08e371edf262cccb62de10e214bd4aaafaa069f1cd49c9c1c3a5ae8e4" +checksum = "317081a0cbbb1f749d348b262575608fc082d47ab11b6247bbe9163eeb955777" dependencies = [ "anyhow", "cranelift-bitset", @@ -7260,14 +7288,15 @@ dependencies = [ [[package]] name = "wasmtime-fiber" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8828d7d8fbe90d087a9edea9223315caf7eb434848896667e5d27889f1173" +checksum = "6763b33eceefc443f6477d84dc8751df5f23d280d7e01f28339fa3ec4b00ff13" dependencies = [ "anyhow", "cc", "cfg-if", - "rustix", + "libc", + "rustix 1.0.8", "wasmtime-asm-macros", "wasmtime-versioned-export-macros", "windows-sys 0.59.0", @@ -7275,9 +7304,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-icache-coherence" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54f6c6c7e9d7eeee32dfcc10db7f29d505ee7dd28d00593ea241d5f70698e64" +checksum = "8ea6b740d1a35f2cebfe88e013ac8a4a84ff8dabc3a392df920abf554e871cf2" dependencies = [ "anyhow", "cfg-if", @@ -7287,24 +7316,24 @@ dependencies = [ [[package]] name = "wasmtime-math" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1108aad2e6965698f9207ea79b80eda2b3dcc57dcb69f4258296d4664ae32cd" +checksum = "62fa317691aedc64aae3a86b3d786e4b2b0007bc0b56e0b6098b8b5a85ab2134" dependencies = [ "libm", ] [[package]] name = "wasmtime-slab" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d6a321317281b721c5530ef733e8596ecc6065035f286ccd155b3fa8e0ab2f" +checksum = "60a06819d24370273021054b50589e3078e7f5cfac15515e58b3fbbebf5e5b39" [[package]] name = "wasmtime-versioned-export-macros" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5732a5c86efce7bca121a61d8c07875f6b85c1607aa86753b40f7f8bd9d3a780" +checksum = "9ca100ed168ffc9b37aefc07a5be440645eab612a2ff6e2ff884e8cc3740e666" dependencies = [ "proc-macro2", "quote", @@ -7313,9 +7342,9 @@ dependencies = [ [[package]] name = "wasmtime-wit-bindgen" -version = "31.0.0" +version = "34.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505c13fa0cac6c43e805347acf1e916c8de54e3790f2c22873c5692964b09b62" +checksum = "233fdcb96f9097be697319ba647ef42bdbdb40e89f04c8ae3713103813b5b793" dependencies = [ "anyhow", "heck 0.5.0", @@ -7370,7 +7399,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.44", ] [[package]] @@ -7411,7 +7440,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -7756,9 +7785,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.226.0" +version = "0.233.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f007722bfd43a2978c5b8b90f02c927dddf0f11c5f5b50929816b3358718cd" +checksum = "f22f1cd55247a2e616870b619766e9522df36b7abafbb29bbeb34b7a9da7e9f0" dependencies = [ "anyhow", "id-arena", diff --git a/Cargo.toml b/Cargo.toml index fb5d86852..863b37d9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -389,7 +389,7 @@ version = "0.3.0" # Open Policy Agent support through WASM [workspace.dependencies.opa-wasm] -version = "0.1.5" +version = "0.1.6" # OpenTelemetry [workspace.dependencies.opentelemetry] diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 7284e6197..c181cd15c 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -17,7 +17,7 @@ use mas_data_model::{SessionExpirationConfig, SiteConfig}; use mas_email::{MailTransport, Mailer}; use mas_handlers::passwords::PasswordManager; use mas_matrix::{HomeserverConnection, ReadOnlyHomeserverConnection}; -use mas_matrix_synapse::SynapseConnection; +use mas_matrix_synapse::{LegacySynapseConnection, SynapseConnection}; use mas_policy::PolicyFactory; use mas_router::UrlBuilder; use mas_storage::{BoxRepositoryFactory, RepositoryAccess, RepositoryFactory}; @@ -469,14 +469,22 @@ pub fn homeserver_connection_from_config( http_client: reqwest::Client, ) -> Arc { match config.kind { - HomeserverKind::Synapse => Arc::new(SynapseConnection::new( + HomeserverKind::Synapse | HomeserverKind::SynapseLegacy => { + Arc::new(LegacySynapseConnection::new( + config.homeserver.clone(), + config.endpoint.clone(), + config.secret.clone(), + http_client, + )) + } + HomeserverKind::SynapseModern => Arc::new(SynapseConnection::new( config.homeserver.clone(), config.endpoint.clone(), config.secret.clone(), http_client, )), HomeserverKind::SynapseReadOnly => { - let connection = SynapseConnection::new( + let connection = LegacySynapseConnection::new( config.homeserver.clone(), config.endpoint.clone(), config.secret.clone(), diff --git a/crates/config/src/sections/matrix.rs b/crates/config/src/sections/matrix.rs index 1cead9ffd..e035b7d79 100644 --- a/crates/config/src/sections/matrix.rs +++ b/crates/config/src/sections/matrix.rs @@ -27,15 +27,25 @@ fn default_endpoint() -> Url { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum HomeserverKind { - /// Homeserver is Synapse + /// Homeserver is Synapse, using the legacy API + /// + /// This will switch to using the modern API in a few releases. #[default] Synapse, - /// Homeserver is Synapse, in read-only mode + /// Homeserver is Synapse, using the legacy API, in read-only mode /// /// This is meant for testing rolling out Matrix Authentication Service with /// no risk of writing data to the homeserver. + /// + /// This will switch to using the modern API in a few releases. SynapseReadOnly, + + /// Homeserver is Synapse, using the legacy API, + SynapseLegacy, + + /// Homeserver is Synapse, with the modern API available + SynapseModern, } /// Configuration related to the Matrix homeserver diff --git a/crates/handlers/src/admin/v1/users/add.rs b/crates/handlers/src/admin/v1/users/add.rs index 299012d91..d67737526 100644 --- a/crates/handlers/src/admin/v1/users/add.rs +++ b/crates/handlers/src/admin/v1/users/add.rs @@ -166,10 +166,7 @@ pub async fn handler( let user = repo.user().add(&mut rng, &clock, params.username).await?; homeserver - .provision_user(&ProvisionRequest::new( - homeserver.mxid(&user.username), - &user.sub, - )) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .map_err(RouteError::Homeserver)?; @@ -222,8 +219,7 @@ mod tests { assert_eq!(user.username, "alice"); // Check that the user was created on the homeserver - let mxid = state.homeserver_connection.mxid("alice"); - let result = state.homeserver_connection.query_user(&mxid).await; + let result = state.homeserver_connection.query_user("alice").await; assert!(result.is_ok()); } diff --git a/crates/handlers/src/admin/v1/users/reactivate.rs b/crates/handlers/src/admin/v1/users/reactivate.rs index 37b38c6b6..0be687a39 100644 --- a/crates/handlers/src/admin/v1/users/reactivate.rs +++ b/crates/handlers/src/admin/v1/users/reactivate.rs @@ -83,9 +83,8 @@ pub async fn handler( .ok_or(RouteError::NotFound(id))?; // Call the homeserver synchronously to reactivate the user - let mxid = homeserver.mxid(&user.username); homeserver - .reactivate_user(&mxid) + .reactivate_user(&user.username) .await .map_err(RouteError::Homeserver)?; @@ -127,20 +126,23 @@ mod tests { // Provision and immediately deactivate the user on the homeserver, // because this endpoint will try to reactivate it - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(&mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); state .homeserver_connection - .delete_user(&mxid, true) + .delete_user(&user.username, true) .await .unwrap(); // The user should be deactivated on the homeserver - let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap(); + let mx_user = state + .homeserver_connection + .query_user(&user.username) + .await + .unwrap(); assert!(mx_user.deactivated); let request = Request::post(format!("/api/admin/v1/users/{}/reactivate", user.id)) @@ -176,10 +178,9 @@ mod tests { repo.save().await.unwrap(); // Provision the user on the homeserver - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(&mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); diff --git a/crates/handlers/src/admin/v1/users/unlock.rs b/crates/handlers/src/admin/v1/users/unlock.rs index 5584f4a69..944dd77f4 100644 --- a/crates/handlers/src/admin/v1/users/unlock.rs +++ b/crates/handlers/src/admin/v1/users/unlock.rs @@ -112,10 +112,9 @@ mod tests { // Also provision the user on the homeserver, because this endpoint will try to // reactivate it - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(&mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); @@ -149,21 +148,24 @@ mod tests { repo.save().await.unwrap(); // Provision the user on the homeserver - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(&mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); // but then deactivate it state .homeserver_connection - .delete_user(&mxid, true) + .delete_user(&user.username, true) .await .unwrap(); // The user should be deactivated on the homeserver - let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap(); + let mx_user = state + .homeserver_connection + .query_user(&user.username) + .await + .unwrap(); assert!(mx_user.deactivated); let request = Request::post(format!("/api/admin/v1/users/{}/unlock", user.id)) @@ -182,7 +184,11 @@ mod tests { body["data"]["attributes"]["deactivated_at"], serde_json::json!(state.clock.now()) ); - let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap(); + let mx_user = state + .homeserver_connection + .query_user(&user.username) + .await + .unwrap(); assert!(mx_user.deactivated); } diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 75b96417e..d75b4f8bd 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -411,7 +411,11 @@ pub(crate) async fn post( // Now we can create the device on the homeserver, without holding the // transaction if let Err(err) = homeserver - .create_device(&user_id, device.as_str(), session.human_name.as_deref()) + .upsert_device( + &user.username, + device.as_str(), + session.human_name.as_deref(), + ) .await { // Something went wrong, let's end this session and schedule a device sync @@ -829,10 +833,9 @@ mod tests { .add(&mut rng, &state.clock, &user, version, hash, None) .await .unwrap(); - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); @@ -1133,10 +1136,9 @@ mod tests { .await .unwrap(); - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); @@ -1239,10 +1241,9 @@ mod tests { let user = repo.user().lock(&state.clock, user).await.unwrap(); repo.save().await.unwrap(); - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); diff --git a/crates/handlers/src/graphql/model/matrix.rs b/crates/handlers/src/graphql/model/matrix.rs index 08e583d74..7316c0d63 100644 --- a/crates/handlers/src/graphql/model/matrix.rs +++ b/crates/handlers/src/graphql/model/matrix.rs @@ -27,9 +27,9 @@ impl MatrixUser { conn: &C, user: &str, ) -> Result { - let mxid = conn.mxid(user); + let info = conn.query_user(user).await?; - let info = conn.query_user(&mxid).await?; + let mxid = conn.mxid(user); Ok(MatrixUser { mxid, diff --git a/crates/handlers/src/graphql/mutations/compat_session.rs b/crates/handlers/src/graphql/mutations/compat_session.rs index ec8993942..973d46f05 100644 --- a/crates/handlers/src/graphql/mutations/compat_session.rs +++ b/crates/handlers/src/graphql/mutations/compat_session.rs @@ -187,10 +187,9 @@ impl CompatSessionMutations { .await?; // Update the device on the homeserver side - let mxid = homeserver.mxid(&user.username); if let Some(device) = session.device.as_ref() { homeserver - .update_device_display_name(&mxid, device.as_str(), &input.human_name) + .update_device_display_name(&user.username, device.as_str(), &input.human_name) .await .context("Failed to provision device")?; } diff --git a/crates/handlers/src/graphql/mutations/matrix.rs b/crates/handlers/src/graphql/mutations/matrix.rs index 16c4916c3..f88668e2f 100644 --- a/crates/handlers/src/graphql/mutations/matrix.rs +++ b/crates/handlers/src/graphql/mutations/matrix.rs @@ -93,7 +93,6 @@ impl MatrixMutations { repo.cancel().await?; let conn = state.homeserver_connection(); - let mxid = conn.mxid(&user.username); if let Some(display_name) = &input.display_name { // Let's do some basic validation on the display name @@ -105,11 +104,11 @@ impl MatrixMutations { return Ok(SetDisplayNamePayload::Invalid); } - conn.set_displayname(&mxid, display_name) + conn.set_displayname(&user.username, display_name) .await .context("Failed to set display name")?; } else { - conn.unset_displayname(&mxid) + conn.unset_displayname(&user.username) .await .context("Failed to unset display name")?; } diff --git a/crates/handlers/src/graphql/mutations/oauth2_session.rs b/crates/handlers/src/graphql/mutations/oauth2_session.rs index 0de5b16a2..55723efc5 100644 --- a/crates/handlers/src/graphql/mutations/oauth2_session.rs +++ b/crates/handlers/src/graphql/mutations/oauth2_session.rs @@ -212,11 +212,10 @@ impl OAuth2SessionMutations { repo.user().acquire_lock_for_sync(&user).await?; // Look for devices to provision - let mxid = homeserver.mxid(&user.username); for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str(), None) + .upsert_device(&user.username, device.as_str(), None) .await .context("Failed to provision device")?; } @@ -331,11 +330,10 @@ impl OAuth2SessionMutations { .await?; // Update the device on the homeserver side - let mxid = homeserver.mxid(&user.username); for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .update_device_display_name(&mxid, device.as_str(), &input.human_name) + .update_device_display_name(&user.username, device.as_str(), &input.human_name) .await .context("Failed to provision device")?; } diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index 26352db81..f9f5696e7 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -586,8 +586,7 @@ impl UserMutations { }; // Call the homeserver synchronously to reactivate the user - let mxid = matrix.mxid(&user.username); - matrix.reactivate_user(&mxid).await?; + matrix.reactivate_user(&user.username).await?; // Now reactivate & unlock the user in our database let user = repo.user().reactivate(user).await?; @@ -654,9 +653,7 @@ impl UserMutations { }; let conn = state.homeserver_connection(); - let mxid = conn.mxid(&user.username); - - conn.allow_cross_signing_reset(&mxid) + conn.allow_cross_signing_reset(&user.username) .await .context("Failed to allow cross-signing reset")?; diff --git a/crates/handlers/src/graphql/tests.rs b/crates/handlers/src/graphql/tests.rs index df4b5b80b..bc5079924 100644 --- a/crates/handlers/src/graphql/tests.rs +++ b/crates/handlers/src/graphql/tests.rs @@ -529,10 +529,9 @@ async fn test_oauth2_client_credentials(pool: PgPool) { // XXX: we don't run the task worker here, so even though the addUser mutation // should have scheduled a job to provision the user, it won't run in the test, // so we need to do it manually - let mxid = state.homeserver_connection.mxid("alice"); state .homeserver_connection - .provision_user(&ProvisionRequest::new(mxid, user_id)) + .provision_user(&ProvisionRequest::new("alice", user_id)) .await .unwrap(); diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index b1a7a99ea..50c043b04 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -634,10 +634,9 @@ mod tests { .await .unwrap(); - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); @@ -835,10 +834,9 @@ mod tests { .await .unwrap(); - let mxid = state.homeserver_connection.mxid(&user.username); state .homeserver_connection - .provision_user(&ProvisionRequest::new(mxid, &user.sub)) + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) .await .unwrap(); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 1634e8f2e..768f79d35 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -575,11 +575,14 @@ async fn authorization_code_grant( .await?; // Look for device to provision - let mxid = homeserver.mxid(&browser_session.user.username); for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str(), Some(&device_name)) + .upsert_device( + &browser_session.user.username, + device.as_str(), + Some(&device_name), + ) .await .map_err(RouteError::ProvisionDeviceFailed)?; } @@ -951,11 +954,10 @@ async fn device_code_grant( .await?; // Look for device to provision - let mxid = homeserver.mxid(&browser_session.user.username); for scope in &*session.scope { if let Some(device) = Device::from_scope_token(scope) { homeserver - .create_device(&mxid, device.as_str(), None) + .upsert_device(&browser_session.user.username, device.as_str(), None) .await .map_err(RouteError::ProvisionDeviceFailed)?; } diff --git a/crates/matrix-synapse/src/error.rs b/crates/matrix-synapse/src/error.rs index 01f01a3ce..c1d98ccd1 100644 --- a/crates/matrix-synapse/src/error.rs +++ b/crates/matrix-synapse/src/error.rs @@ -9,6 +9,16 @@ use async_trait::async_trait; use serde::Deserialize; use thiserror::Error; +/// Encountered when trying to register a user ID which has been taken. +/// — +pub(crate) const M_USER_IN_USE: &str = "M_USER_IN_USE"; +/// Encountered when trying to register a user ID which is not valid. +/// — +pub(crate) const M_INVALID_USERNAME: &str = "M_INVALID_USERNAME"; +/// Encountered when trying to register a user ID reserved by an appservice. +/// — +pub(crate) const M_EXCLUSIVE: &str = "M_EXCLUSIVE"; + /// Represents a Matrix error /// Ref: #[derive(Debug, Deserialize)] diff --git a/crates/matrix-synapse/src/legacy.rs b/crates/matrix-synapse/src/legacy.rs new file mode 100644 index 000000000..d07e6b5d5 --- /dev/null +++ b/crates/matrix-synapse/src/legacy.rs @@ -0,0 +1,683 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. + +use std::{collections::HashSet, time::Duration}; + +use anyhow::{Context, bail}; +use http::{Method, StatusCode}; +use mas_http::RequestBuilderExt as _; +use mas_matrix::{HomeserverConnection, MatrixUser, ProvisionRequest}; +use serde::{Deserialize, Serialize}; +use tracing::debug; +use url::Url; + +use crate::error::{M_EXCLUSIVE, M_INVALID_USERNAME, M_USER_IN_USE, SynapseResponseExt}; + +static SYNAPSE_AUTH_PROVIDER: &str = "oauth-delegated"; + +#[derive(Clone)] +pub struct SynapseConnection { + homeserver: String, + endpoint: Url, + access_token: String, + http_client: reqwest::Client, +} + +impl SynapseConnection { + #[must_use] + pub fn new( + homeserver: String, + endpoint: Url, + access_token: String, + http_client: reqwest::Client, + ) -> Self { + Self { + homeserver, + endpoint, + access_token, + http_client, + } + } + + fn builder(&self, method: Method, url: &str) -> reqwest::RequestBuilder { + self.http_client + .request( + method, + self.endpoint + .join(url) + .map(String::from) + .unwrap_or_default(), + ) + .bearer_auth(&self.access_token) + } + + fn post(&self, url: &str) -> reqwest::RequestBuilder { + self.builder(Method::POST, url) + } + + fn get(&self, url: &str) -> reqwest::RequestBuilder { + self.builder(Method::GET, url) + } + + fn put(&self, url: &str) -> reqwest::RequestBuilder { + self.builder(Method::PUT, url) + } + + fn delete(&self, url: &str) -> reqwest::RequestBuilder { + self.builder(Method::DELETE, url) + } +} + +#[derive(Serialize, Deserialize)] +struct ExternalID { + auth_provider: String, + external_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum ThreePIDMedium { + Email, + Msisdn, +} + +#[derive(Serialize, Deserialize)] +struct ThreePID { + medium: ThreePIDMedium, + address: String, +} + +#[derive(Default, Serialize, Deserialize)] +struct SynapseUser { + #[serde( + default, + rename = "displayname", + skip_serializing_if = "Option::is_none" + )] + display_name: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + avatar_url: Option, + + #[serde(default, rename = "threepids", skip_serializing_if = "Option::is_none")] + three_pids: Option>, + + #[serde(default, skip_serializing_if = "Option::is_none")] + external_ids: Option>, + + #[serde(default, skip_serializing_if = "Option::is_none")] + deactivated: Option, +} + +#[derive(Deserialize)] +struct SynapseDeviceListResponse { + devices: Vec, +} + +#[derive(Serialize, Deserialize)] +struct SynapseDevice { + device_id: String, + + #[serde(default, skip_serializing_if = "Option::is_none")] + dehydrated: Option, +} + +#[derive(Serialize)] +struct SynapseUpdateDeviceRequest<'a> { + display_name: Option<&'a str>, +} + +#[derive(Serialize)] +struct SynapseDeleteDevicesRequest { + devices: Vec, +} + +#[derive(Serialize)] +struct SetDisplayNameRequest<'a> { + displayname: &'a str, +} + +#[derive(Serialize)] +struct SynapseDeactivateUserRequest { + erase: bool, +} + +#[derive(Serialize)] +struct SynapseAllowCrossSigningResetRequest {} + +/// Response body of +/// `/_synapse/admin/v1/username_available?username={localpart}` +#[derive(Deserialize)] +struct UsernameAvailableResponse { + available: bool, +} + +#[async_trait::async_trait] +impl HomeserverConnection for SynapseConnection { + fn homeserver(&self) -> &str { + &self.homeserver + } + + #[tracing::instrument( + name = "homeserver.query_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn query_user(&self, localpart: &str) -> Result { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + + let response = self + .get(&format!("_synapse/admin/v2/users/{encoded_mxid}")) + .send_traced() + .await + .context("Failed to query user from Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while querying user from Synapse")?; + + let body: SynapseUser = response + .json() + .await + .context("Failed to deserialize response while querying user from Synapse")?; + + Ok(MatrixUser { + displayname: body.display_name, + avatar_url: body.avatar_url, + deactivated: body.deactivated.unwrap_or(false), + }) + } + + #[tracing::instrument( + name = "homeserver.is_localpart_available", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn is_localpart_available(&self, localpart: &str) -> Result { + // Synapse will give us a M_UNKNOWN error if the localpart is not ASCII, + // so we bail out early + if !localpart.is_ascii() { + return Ok(false); + } + + let localpart = urlencoding::encode(localpart); + + let response = self + .get(&format!( + "_synapse/admin/v1/username_available?username={localpart}" + )) + .send_traced() + .await + .context("Failed to query localpart availability from Synapse")?; + + match response.error_for_synapse_error().await { + Ok(resp) => { + let response: UsernameAvailableResponse = resp.json().await.context( + "Unexpected response while querying localpart availability from Synapse", + )?; + + Ok(response.available) + } + + Err(err) + if err.errcode() == Some(M_INVALID_USERNAME) + || err.errcode() == Some(M_USER_IN_USE) + || err.errcode() == Some(M_EXCLUSIVE) => + { + debug!( + error = &err as &dyn std::error::Error, + "Localpart is not available" + ); + Ok(false) + } + + Err(err) => Err(err).context("Failed to query localpart availability from Synapse"), + } + } + + #[tracing::instrument( + name = "homeserver.provision_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = request.localpart(), + user.id = request.sub(), + ), + err(Debug), + )] + async fn provision_user(&self, request: &ProvisionRequest) -> Result { + let mut body = SynapseUser { + external_ids: Some(vec![ExternalID { + auth_provider: SYNAPSE_AUTH_PROVIDER.to_owned(), + external_id: request.sub().to_owned(), + }]), + ..SynapseUser::default() + }; + + request + .on_displayname(|displayname| { + body.display_name = Some(displayname.unwrap_or_default().to_owned()); + }) + .on_avatar_url(|avatar_url| { + body.avatar_url = Some(avatar_url.unwrap_or_default().to_owned()); + }) + .on_emails(|emails| { + body.three_pids = Some( + emails + .unwrap_or_default() + .iter() + .map(|email| ThreePID { + medium: ThreePIDMedium::Email, + address: email.clone(), + }) + .collect(), + ); + }); + + let mxid = self.mxid(request.localpart()); + let encoded_mxid = urlencoding::encode(&mxid); + let response = self + .put(&format!("_synapse/admin/v2/users/{encoded_mxid}")) + .json(&body) + .send_traced() + .await + .context("Failed to provision user in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while provisioning user in Synapse")?; + + match response.status() { + StatusCode::CREATED => Ok(true), + StatusCode::OK => Ok(false), + code => bail!("Unexpected HTTP code while provisioning user in Synapse: {code}"), + } + } + + #[tracing::instrument( + name = "homeserver.upsert_device", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn upsert_device( + &self, + localpart: &str, + device_id: &str, + initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + + let response = self + .post(&format!("_synapse/admin/v2/users/{encoded_mxid}/devices")) + .json(&SynapseDevice { + device_id: device_id.to_owned(), + dehydrated: None, + }) + .send_traced() + .await + .context("Failed to create device in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while creating device in Synapse")?; + + if response.status() != StatusCode::CREATED { + bail!( + "Unexpected HTTP code while creating device in Synapse: {}", + response.status() + ); + } + + // It's annoying, but the POST endpoint doesn't let us set the display name + // of the device, so we have to do it manually. + if let Some(display_name) = initial_display_name { + self.update_device_display_name(localpart, device_id, display_name) + .await?; + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.update_device_display_name", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn update_device_display_name( + &self, + localpart: &str, + device_id: &str, + display_name: &str, + ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + let device_id = urlencoding::encode(device_id); + let response = self + .put(&format!( + "_synapse/admin/v2/users/{encoded_mxid}/devices/{device_id}" + )) + .json(&SynapseUpdateDeviceRequest { + display_name: Some(display_name), + }) + .send_traced() + .await + .context("Failed to update device display name in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while updating device display name in Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while updating device display name in Synapse: {}", + response.status() + ); + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.delete_device", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn delete_device(&self, localpart: &str, device_id: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + let encoded_device_id = urlencoding::encode(device_id); + + let response = self + .delete(&format!( + "_synapse/admin/v2/users/{encoded_mxid}/devices/{encoded_device_id}" + )) + .send_traced() + .await + .context("Failed to delete device in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while deleting device in Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while deleting device in Synapse: {}", + response.status() + ); + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.sync_devices", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn sync_devices( + &self, + localpart: &str, + devices: HashSet, + ) -> Result<(), anyhow::Error> { + // Get the list of current devices + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + + let response = self + .get(&format!("_synapse/admin/v2/users/{encoded_mxid}/devices")) + .send_traced() + .await + .context("Failed to query devices from Synapse")?; + + let response = response.error_for_synapse_error().await?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while querying devices from Synapse: {}", + response.status() + ); + } + + let body: SynapseDeviceListResponse = response + .json() + .await + .context("Failed to parse response while querying devices from Synapse")?; + + let existing_devices: HashSet = body + .devices + .into_iter() + .filter(|d| d.dehydrated != Some(true)) + .map(|d| d.device_id) + .collect(); + + // First, delete all the devices that are not needed anymore + let to_delete = existing_devices.difference(&devices).cloned().collect(); + + let response = self + .post(&format!( + "_synapse/admin/v2/users/{encoded_mxid}/delete_devices" + )) + .json(&SynapseDeleteDevicesRequest { devices: to_delete }) + .send_traced() + .await + .context("Failed to delete devices from Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while deleting devices from Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while deleting devices from Synapse: {}", + response.status() + ); + } + + // Then, create the devices that are missing. There is no batching API to do + // this, so we do this sequentially, which is fine as the API is idempotent. + for device_id in devices.difference(&existing_devices) { + self.upsert_device(localpart, device_id, None).await?; + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.delete_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + erase = erase, + ), + err(Debug), + )] + async fn delete_user(&self, localpart: &str, erase: bool) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + + let response = self + .post(&format!("_synapse/admin/v1/deactivate/{encoded_mxid}")) + .json(&SynapseDeactivateUserRequest { erase }) + // Deactivation can take a while, so we set a longer timeout + .timeout(Duration::from_secs(60 * 5)) + .send_traced() + .await + .context("Failed to deactivate user in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while deactivating user in Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while deactivating user in Synapse: {}", + response.status() + ); + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.reactivate_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn reactivate_user(&self, localpart: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + let response = self + .put(&format!("_synapse/admin/v2/users/{encoded_mxid}")) + .json(&SynapseUser { + deactivated: Some(false), + ..SynapseUser::default() + }) + .send_traced() + .await + .context("Failed to reactivate user in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while reactivating user in Synapse")?; + + match response.status() { + StatusCode::CREATED | StatusCode::OK => Ok(()), + code => bail!("Unexpected HTTP code while reactivating user in Synapse: {code}",), + } + } + + #[tracing::instrument( + name = "homeserver.set_displayname", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.displayname = displayname, + ), + err(Debug), + )] + async fn set_displayname( + &self, + localpart: &str, + displayname: &str, + ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + let response = self + .put(&format!( + "_matrix/client/v3/profile/{encoded_mxid}/displayname" + )) + .json(&SetDisplayNameRequest { displayname }) + .send_traced() + .await + .context("Failed to set displayname in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while setting displayname in Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while setting displayname in Synapse: {}", + response.status() + ); + } + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.unset_displayname", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Display), + )] + async fn unset_displayname(&self, localpart: &str) -> Result<(), anyhow::Error> { + self.set_displayname(localpart, "").await + } + + #[tracing::instrument( + name = "homeserver.allow_cross_signing_reset", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn allow_cross_signing_reset(&self, localpart: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); + let encoded_mxid = urlencoding::encode(&mxid); + + let response = self + .post(&format!( + "_synapse/admin/v1/users/{encoded_mxid}/_allow_cross_signing_replacement_without_uia" + )) + .json(&SynapseAllowCrossSigningResetRequest {}) + .send_traced() + .await + .context("Failed to allow cross-signing reset in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while allowing cross-signing reset in Synapse")?; + + if response.status() != StatusCode::OK { + bail!( + "Unexpected HTTP code while allowing cross-signing reset in Synapse: {}", + response.status(), + ); + } + + Ok(()) + } +} diff --git a/crates/matrix-synapse/src/lib.rs b/crates/matrix-synapse/src/lib.rs index 28f27d76c..062ecaa75 100644 --- a/crates/matrix-synapse/src/lib.rs +++ b/crates/matrix-synapse/src/lib.rs @@ -4,678 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. -use std::{collections::HashSet, time::Duration}; - -use anyhow::{Context, bail}; -use error::SynapseResponseExt; -use http::{Method, StatusCode}; -use mas_http::RequestBuilderExt as _; -use mas_matrix::{HomeserverConnection, MatrixUser, ProvisionRequest}; -use serde::{Deserialize, Serialize}; -use tracing::debug; -use url::Url; - -static SYNAPSE_AUTH_PROVIDER: &str = "oauth-delegated"; - -/// Encountered when trying to register a user ID which has been taken. -/// — -const M_USER_IN_USE: &str = "M_USER_IN_USE"; -/// Encountered when trying to register a user ID which is not valid. -/// — -const M_INVALID_USERNAME: &str = "M_INVALID_USERNAME"; -/// Encountered when trying to register a user ID reserved by an appservice. -/// — -const M_EXCLUSIVE: &str = "M_EXCLUSIVE"; - mod error; +mod legacy; +mod modern; -#[derive(Clone)] -pub struct SynapseConnection { - homeserver: String, - endpoint: Url, - access_token: String, - http_client: reqwest::Client, -} - -impl SynapseConnection { - #[must_use] - pub fn new( - homeserver: String, - endpoint: Url, - access_token: String, - http_client: reqwest::Client, - ) -> Self { - Self { - homeserver, - endpoint, - access_token, - http_client, - } - } - - fn builder(&self, method: Method, url: &str) -> reqwest::RequestBuilder { - self.http_client - .request( - method, - self.endpoint - .join(url) - .map(String::from) - .unwrap_or_default(), - ) - .bearer_auth(&self.access_token) - } - - fn post(&self, url: &str) -> reqwest::RequestBuilder { - self.builder(Method::POST, url) - } - - fn get(&self, url: &str) -> reqwest::RequestBuilder { - self.builder(Method::GET, url) - } - - fn put(&self, url: &str) -> reqwest::RequestBuilder { - self.builder(Method::PUT, url) - } - - fn delete(&self, url: &str) -> reqwest::RequestBuilder { - self.builder(Method::DELETE, url) - } -} - -#[derive(Serialize, Deserialize)] -struct ExternalID { - auth_provider: String, - external_id: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -enum ThreePIDMedium { - Email, - Msisdn, -} - -#[derive(Serialize, Deserialize)] -struct ThreePID { - medium: ThreePIDMedium, - address: String, -} - -#[derive(Default, Serialize, Deserialize)] -struct SynapseUser { - #[serde( - default, - rename = "displayname", - skip_serializing_if = "Option::is_none" - )] - display_name: Option, - - #[serde(default, skip_serializing_if = "Option::is_none")] - avatar_url: Option, - - #[serde(default, rename = "threepids", skip_serializing_if = "Option::is_none")] - three_pids: Option>, - - #[serde(default, skip_serializing_if = "Option::is_none")] - external_ids: Option>, - - #[serde(default, skip_serializing_if = "Option::is_none")] - deactivated: Option, -} - -#[derive(Deserialize)] -struct SynapseDeviceListResponse { - devices: Vec, -} - -#[derive(Serialize, Deserialize)] -struct SynapseDevice { - device_id: String, - - #[serde(default, skip_serializing_if = "Option::is_none")] - dehydrated: Option, -} - -#[derive(Serialize)] -struct SynapseUpdateDeviceRequest<'a> { - display_name: Option<&'a str>, -} - -#[derive(Serialize)] -struct SynapseDeleteDevicesRequest { - devices: Vec, -} - -#[derive(Serialize)] -struct SetDisplayNameRequest<'a> { - displayname: &'a str, -} - -#[derive(Serialize)] -struct SynapseDeactivateUserRequest { - erase: bool, -} - -#[derive(Serialize)] -struct SynapseAllowCrossSigningResetRequest {} - -/// Response body of -/// `/_synapse/admin/v1/username_available?username={localpart}` -#[derive(Deserialize)] -struct UsernameAvailableResponse { - available: bool, -} - -#[async_trait::async_trait] -impl HomeserverConnection for SynapseConnection { - fn homeserver(&self) -> &str { - &self.homeserver - } - - #[tracing::instrument( - name = "homeserver.query_user", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - ), - err(Debug), - )] - async fn query_user(&self, mxid: &str) -> Result { - let encoded_mxid = urlencoding::encode(mxid); - - let response = self - .get(&format!("_synapse/admin/v2/users/{encoded_mxid}")) - .send_traced() - .await - .context("Failed to query user from Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while querying user from Synapse")?; - - let body: SynapseUser = response - .json() - .await - .context("Failed to deserialize response while querying user from Synapse")?; - - Ok(MatrixUser { - displayname: body.display_name, - avatar_url: body.avatar_url, - deactivated: body.deactivated.unwrap_or(false), - }) - } - - #[tracing::instrument( - name = "homeserver.is_localpart_available", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.localpart = localpart, - ), - err(Debug), - )] - async fn is_localpart_available(&self, localpart: &str) -> Result { - // Synapse will give us a M_UNKNOWN error if the localpart is not ASCII, - // so we bail out early - if !localpart.is_ascii() { - return Ok(false); - } - - let localpart = urlencoding::encode(localpart); - - let response = self - .get(&format!( - "_synapse/admin/v1/username_available?username={localpart}" - )) - .send_traced() - .await - .context("Failed to query localpart availability from Synapse")?; - - match response.error_for_synapse_error().await { - Ok(resp) => { - let response: UsernameAvailableResponse = resp.json().await.context( - "Unexpected response while querying localpart availability from Synapse", - )?; - - Ok(response.available) - } - - Err(err) - if err.errcode() == Some(M_INVALID_USERNAME) - || err.errcode() == Some(M_USER_IN_USE) - || err.errcode() == Some(M_EXCLUSIVE) => - { - debug!( - error = &err as &dyn std::error::Error, - "Localpart is not available" - ); - Ok(false) - } - - Err(err) => Err(err).context("Failed to query localpart availability from Synapse"), - } - } - - #[tracing::instrument( - name = "homeserver.provision_user", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = request.mxid(), - user.id = request.sub(), - ), - err(Debug), - )] - async fn provision_user(&self, request: &ProvisionRequest) -> Result { - let mut body = SynapseUser { - external_ids: Some(vec![ExternalID { - auth_provider: SYNAPSE_AUTH_PROVIDER.to_owned(), - external_id: request.sub().to_owned(), - }]), - ..SynapseUser::default() - }; - - request - .on_displayname(|displayname| { - body.display_name = Some(displayname.unwrap_or_default().to_owned()); - }) - .on_avatar_url(|avatar_url| { - body.avatar_url = Some(avatar_url.unwrap_or_default().to_owned()); - }) - .on_emails(|emails| { - body.three_pids = Some( - emails - .unwrap_or_default() - .iter() - .map(|email| ThreePID { - medium: ThreePIDMedium::Email, - address: email.clone(), - }) - .collect(), - ); - }); - - let encoded_mxid = urlencoding::encode(request.mxid()); - let response = self - .put(&format!("_synapse/admin/v2/users/{encoded_mxid}")) - .json(&body) - .send_traced() - .await - .context("Failed to provision user in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while provisioning user in Synapse")?; - - match response.status() { - StatusCode::CREATED => Ok(true), - StatusCode::OK => Ok(false), - code => bail!("Unexpected HTTP code while provisioning user in Synapse: {code}"), - } - } - - #[tracing::instrument( - name = "homeserver.create_device", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - matrix.device_id = device_id, - ), - err(Debug), - )] - async fn create_device( - &self, - mxid: &str, - device_id: &str, - initial_display_name: Option<&str>, - ) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - - let response = self - .post(&format!("_synapse/admin/v2/users/{encoded_mxid}/devices")) - .json(&SynapseDevice { - device_id: device_id.to_owned(), - dehydrated: None, - }) - .send_traced() - .await - .context("Failed to create device in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while creating device in Synapse")?; - - if response.status() != StatusCode::CREATED { - bail!( - "Unexpected HTTP code while creating device in Synapse: {}", - response.status() - ); - } - - // It's annoying, but the POST endpoint doesn't let us set the display name - // of the device, so we have to do it manually. - if let Some(display_name) = initial_display_name { - self.update_device_display_name(mxid, device_id, display_name) - .await?; - } - - Ok(()) - } - - #[tracing::instrument( - name = "homeserver.update_device_display_name", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - matrix.device_id = device_id, - ), - err(Debug), - )] - async fn update_device_display_name( - &self, - mxid: &str, - device_id: &str, - display_name: &str, - ) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - let device_id = urlencoding::encode(device_id); - let response = self - .put(&format!( - "_synapse/admin/v2/users/{encoded_mxid}/devices/{device_id}" - )) - .json(&SynapseUpdateDeviceRequest { - display_name: Some(display_name), - }) - .send_traced() - .await - .context("Failed to update device display name in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while updating device display name in Synapse")?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while updating device display name in Synapse: {}", - response.status() - ); - } - - Ok(()) - } - - #[tracing::instrument( - name = "homeserver.delete_device", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - matrix.device_id = device_id, - ), - err(Debug), - )] - async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - let encoded_device_id = urlencoding::encode(device_id); - - let response = self - .delete(&format!( - "_synapse/admin/v2/users/{encoded_mxid}/devices/{encoded_device_id}" - )) - .send_traced() - .await - .context("Failed to delete device in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while deleting device in Synapse")?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while deleting device in Synapse: {}", - response.status() - ); - } - - Ok(()) - } - - #[tracing::instrument( - name = "homeserver.sync_devices", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - ), - err(Debug), - )] - async fn sync_devices( - &self, - mxid: &str, - devices: HashSet, - ) -> Result<(), anyhow::Error> { - // Get the list of current devices - let encoded_mxid = urlencoding::encode(mxid); - - let response = self - .get(&format!("_synapse/admin/v2/users/{encoded_mxid}/devices")) - .send_traced() - .await - .context("Failed to query devices from Synapse")?; - - let response = response.error_for_synapse_error().await?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while querying devices from Synapse: {}", - response.status() - ); - } - - let body: SynapseDeviceListResponse = response - .json() - .await - .context("Failed to parse response while querying devices from Synapse")?; - - let existing_devices: HashSet = body - .devices - .into_iter() - .filter(|d| d.dehydrated != Some(true)) - .map(|d| d.device_id) - .collect(); - - // First, delete all the devices that are not needed anymore - let to_delete = existing_devices.difference(&devices).cloned().collect(); - - let response = self - .post(&format!( - "_synapse/admin/v2/users/{encoded_mxid}/delete_devices" - )) - .json(&SynapseDeleteDevicesRequest { devices: to_delete }) - .send_traced() - .await - .context("Failed to delete devices from Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while deleting devices from Synapse")?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while deleting devices from Synapse: {}", - response.status() - ); - } - - // Then, create the devices that are missing. There is no batching API to do - // this, so we do this sequentially, which is fine as the API is idempotent. - for device_id in devices.difference(&existing_devices) { - self.create_device(mxid, device_id, None).await?; - } - - Ok(()) - } - - #[tracing::instrument( - name = "homeserver.delete_user", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - erase = erase, - ), - err(Debug), - )] - async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - - let response = self - .post(&format!("_synapse/admin/v1/deactivate/{encoded_mxid}")) - .json(&SynapseDeactivateUserRequest { erase }) - // Deactivation can take a while, so we set a longer timeout - .timeout(Duration::from_secs(60 * 5)) - .send_traced() - .await - .context("Failed to deactivate user in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while deactivating user in Synapse")?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while deactivating user in Synapse: {}", - response.status() - ); - } - - Ok(()) - } - - #[tracing::instrument( - name = "homeserver.reactivate_user", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - ), - err(Debug), - )] - async fn reactivate_user(&self, mxid: &str) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - let response = self - .put(&format!("_synapse/admin/v2/users/{encoded_mxid}")) - .json(&SynapseUser { - deactivated: Some(false), - ..SynapseUser::default() - }) - .send_traced() - .await - .context("Failed to reactivate user in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while reactivating user in Synapse")?; - - match response.status() { - StatusCode::CREATED | StatusCode::OK => Ok(()), - code => bail!("Unexpected HTTP code while reactivating user in Synapse: {code}",), - } - } - - #[tracing::instrument( - name = "homeserver.set_displayname", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - matrix.displayname = displayname, - ), - err(Debug), - )] - async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - let response = self - .put(&format!( - "_matrix/client/v3/profile/{encoded_mxid}/displayname" - )) - .json(&SetDisplayNameRequest { displayname }) - .send_traced() - .await - .context("Failed to set displayname in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while setting displayname in Synapse")?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while setting displayname in Synapse: {}", - response.status() - ); - } - - Ok(()) - } - - #[tracing::instrument( - name = "homeserver.unset_displayname", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - ), - err(Display), - )] - async fn unset_displayname(&self, mxid: &str) -> Result<(), anyhow::Error> { - self.set_displayname(mxid, "").await - } - - #[tracing::instrument( - name = "homeserver.allow_cross_signing_reset", - skip_all, - fields( - matrix.homeserver = self.homeserver, - matrix.mxid = mxid, - ), - err(Debug), - )] - async fn allow_cross_signing_reset(&self, mxid: &str) -> Result<(), anyhow::Error> { - let encoded_mxid = urlencoding::encode(mxid); - - let response = self - .post(&format!( - "_synapse/admin/v1/users/{encoded_mxid}/_allow_cross_signing_replacement_without_uia" - )) - .json(&SynapseAllowCrossSigningResetRequest {}) - .send_traced() - .await - .context("Failed to allow cross-signing reset in Synapse")?; - - let response = response - .error_for_synapse_error() - .await - .context("Unexpected HTTP response while allowing cross-signing reset in Synapse")?; - - if response.status() != StatusCode::OK { - bail!( - "Unexpected HTTP code while allowing cross-signing reset in Synapse: {}", - response.status(), - ); - } - - Ok(()) - } -} +pub use self::{legacy::SynapseConnection as LegacySynapseConnection, modern::SynapseConnection}; diff --git a/crates/matrix-synapse/src/modern.rs b/crates/matrix-synapse/src/modern.rs new file mode 100644 index 000000000..26c8e21a1 --- /dev/null +++ b/crates/matrix-synapse/src/modern.rs @@ -0,0 +1,562 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. + +use std::collections::HashSet; + +use anyhow::Context as _; +use http::{Method, StatusCode}; +use mas_http::RequestBuilderExt; +use mas_matrix::{HomeserverConnection, MatrixUser, ProvisionRequest}; +use serde::{Deserialize, Serialize}; +use tracing::debug; +use url::Url; + +use crate::error::{M_EXCLUSIVE, M_INVALID_USERNAME, M_USER_IN_USE, SynapseResponseExt as _}; + +#[derive(Clone)] +pub struct SynapseConnection { + homeserver: String, + endpoint: Url, + access_token: String, + http_client: reqwest::Client, +} + +impl SynapseConnection { + #[must_use] + pub fn new( + homeserver: String, + endpoint: Url, + access_token: String, + http_client: reqwest::Client, + ) -> Self { + Self { + homeserver, + endpoint, + access_token, + http_client, + } + } + + fn builder(&self, method: Method, url: &str) -> reqwest::RequestBuilder { + self.http_client + .request( + method, + self.endpoint + .join(url) + .map(String::from) + .unwrap_or_default(), + ) + .bearer_auth(&self.access_token) + } + + fn post(&self, url: &str) -> reqwest::RequestBuilder { + self.builder(Method::POST, url) + } + + fn get(&self, url: &str) -> reqwest::RequestBuilder { + self.builder(Method::GET, url) + } +} + +#[async_trait::async_trait] +impl HomeserverConnection for SynapseConnection { + fn homeserver(&self) -> &str { + &self.homeserver + } + + #[tracing::instrument( + name = "homeserver.query_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn query_user(&self, localpart: &str) -> Result { + #[derive(Deserialize)] + #[allow(dead_code)] + struct Response { + user_id: String, + display_name: Option, + avatar_url: Option, + is_suspended: bool, + is_deactivated: bool, + } + + let encoded_localpart = urlencoding::encode(localpart); + let url = format!("_synapse/mas/query_user?localpart={encoded_localpart}"); + let response = self + .get(&url) + .send_traced() + .await + .context("Failed to query user from Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while querying user from Synapse")?; + + let body: Response = response + .json() + .await + .context("Failed to deserialize response while querying user from Synapse")?; + + Ok(MatrixUser { + displayname: body.display_name, + avatar_url: body.avatar_url, + deactivated: body.is_deactivated, + }) + } + + #[tracing::instrument( + name = "homeserver.provision_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = request.localpart(), + ), + err(Debug), + )] + async fn provision_user(&self, request: &ProvisionRequest) -> Result { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + set_displayname: Option, + #[serde(skip_serializing_if = "std::ops::Not::not")] + unset_displayname: bool, + #[serde(skip_serializing_if = "Option::is_none")] + set_avatar_url: Option, + #[serde(skip_serializing_if = "std::ops::Not::not")] + unset_avatar_url: bool, + #[serde(skip_serializing_if = "Option::is_none")] + set_emails: Option>, + #[serde(skip_serializing_if = "std::ops::Not::not")] + unset_emails: bool, + } + + let mut body = Request { + localpart: request.localpart(), + set_displayname: None, + unset_displayname: false, + set_avatar_url: None, + unset_avatar_url: false, + set_emails: None, + unset_emails: false, + }; + + request.on_displayname(|displayname| match displayname { + Some(name) => body.set_displayname = Some(name.to_owned()), + None => body.unset_displayname = true, + }); + + request.on_avatar_url(|avatar_url| match avatar_url { + Some(url) => body.set_avatar_url = Some(url.to_owned()), + None => body.unset_avatar_url = true, + }); + + request.on_emails(|emails| match emails { + Some(emails) => body.set_emails = Some(emails.to_owned()), + None => body.unset_emails = true, + }); + + let response = self + .post("_synapse/mas/provision_user") + .json(&body) + .send_traced() + .await + .context("Failed to provision user in Synapse")?; + + let response = response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while provisioning user in Synapse")?; + + match response.status() { + StatusCode::CREATED => Ok(true), + StatusCode::OK => Ok(false), + code => { + anyhow::bail!("Unexpected HTTP code while provisioning user in Synapse: {code}") + } + } + } + + #[tracing::instrument( + name = "homeserver.is_localpart_available", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn is_localpart_available(&self, localpart: &str) -> Result { + // Synapse will give us an error if the localpart is not ASCII, so we bail out + // early + if !localpart.is_ascii() { + return Ok(false); + } + + let encoded_localpart = urlencoding::encode(localpart); + let url = format!("_synapse/mas/is_localpart_available?localpart={encoded_localpart}"); + let response = self + .get(&url) + .send_traced() + .await + .context("Failed to check localpart availability from Synapse")?; + + match response.error_for_synapse_error().await { + Ok(_resp) => Ok(true), + Err(err) + if err.errcode() == Some(M_INVALID_USERNAME) + || err.errcode() == Some(M_USER_IN_USE) + || err.errcode() == Some(M_EXCLUSIVE) => + { + debug!( + error = &err as &dyn std::error::Error, + "Localpart is not available" + ); + Ok(false) + } + + Err(err) => Err(err).context("Failed to query localpart availability from Synapse"), + } + } + + #[tracing::instrument( + name = "homeserver.upsert_device", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn upsert_device( + &self, + localpart: &str, + device_id: &str, + initial_display_name: Option<&str>, + ) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + device_id: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + display_name: Option<&'a str>, + } + + let body = Request { + localpart, + device_id, + display_name: initial_display_name, + }; + + let response = self + .post("_synapse/mas/upsert_device") + .json(&body) + .send_traced() + .await + .context("Failed to create device in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while creating device in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.update_device_display_name", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn update_device_display_name( + &self, + localpart: &str, + device_id: &str, + display_name: &str, + ) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + device_id: &'a str, + display_name: &'a str, + } + + let body = Request { + localpart, + device_id, + display_name, + }; + + let response = self + .post("_synapse/mas/update_device_display_name") + .json(&body) + .send_traced() + .await + .context("Failed to update device display name in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while updating device display name in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.delete_device", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_id = device_id, + ), + err(Debug), + )] + async fn delete_device(&self, localpart: &str, device_id: &str) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + device_id: &'a str, + } + + let body = Request { + localpart, + device_id, + }; + + let response = self + .post("_synapse/mas/delete_device") + .json(&body) + .send_traced() + .await + .context("Failed to delete device in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while deleting device in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.sync_devices", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.device_count = devices.len(), + ), + err(Debug), + )] + async fn sync_devices( + &self, + localpart: &str, + devices: HashSet, + ) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + devices: HashSet, + } + + let body = Request { localpart, devices }; + + let response = self + .post("_synapse/mas/sync_devices") + .json(&body) + .send_traced() + .await + .context("Failed to sync devices in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while syncing devices in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.delete_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + matrix.erase = erase, + ), + err(Debug), + )] + async fn delete_user(&self, localpart: &str, erase: bool) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + erase: bool, + } + + let body = Request { localpart, erase }; + + let response = self + .post("_synapse/mas/delete_user") + .json(&body) + .send_traced() + .await + .context("Failed to delete user in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while deleting user in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.reactivate_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn reactivate_user(&self, localpart: &str) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + } + + let body = Request { localpart }; + + let response = self + .post("_synapse/mas/reactivate_user") + .json(&body) + .send_traced() + .await + .context("Failed to reactivate user in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while reactivating user in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.set_displayname", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn set_displayname( + &self, + localpart: &str, + displayname: &str, + ) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + displayname: &'a str, + } + + let body = Request { + localpart, + displayname, + }; + + let response = self + .post("_synapse/mas/set_displayname") + .json(&body) + .send_traced() + .await + .context("Failed to set displayname in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while setting displayname in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.unset_displayname", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn unset_displayname(&self, localpart: &str) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + } + + let body = Request { localpart }; + + let response = self + .post("_synapse/mas/unset_displayname") + .json(&body) + .send_traced() + .await + .context("Failed to unset displayname in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while unsetting displayname in Synapse")?; + + Ok(()) + } + + #[tracing::instrument( + name = "homeserver.allow_cross_signing_reset", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.localpart = localpart, + ), + err(Debug), + )] + async fn allow_cross_signing_reset(&self, localpart: &str) -> Result<(), anyhow::Error> { + #[derive(Serialize)] + struct Request<'a> { + localpart: &'a str, + } + + let body = Request { localpart }; + + let response = self + .post("_synapse/mas/allow_cross_signing_reset") + .json(&body) + .send_traced() + .await + .context("Failed to allow cross-signing reset in Synapse")?; + + response + .error_for_synapse_error() + .await + .context("Unexpected HTTP response while allowing cross-signing reset in Synapse")?; + + Ok(()) + } +} diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 9c0f81bd3..34c502f81 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -31,7 +31,7 @@ enum FieldAction { } pub struct ProvisionRequest { - mxid: String, + localpart: String, sub: String, displayname: FieldAction, avatar_url: FieldAction, @@ -43,12 +43,12 @@ impl ProvisionRequest { /// /// # Parameters /// - /// * `mxid` - The Matrix ID to provision. + /// * `localpart` - The localpart of the user to provision. /// * `sub` - The `sub` of the user, aka the internal ID. #[must_use] - pub fn new(mxid: impl Into, sub: impl Into) -> Self { + pub fn new(localpart: impl Into, sub: impl Into) -> Self { Self { - mxid: mxid.into(), + localpart: localpart.into(), sub: sub.into(), displayname: FieldAction::DoNothing, avatar_url: FieldAction::DoNothing, @@ -62,10 +62,10 @@ impl ProvisionRequest { &self.sub } - /// Get the Matrix ID to provision. + /// Get the localpart of the user to provision. #[must_use] - pub fn mxid(&self) -> &str { - &self.mxid + pub fn localpart(&self) -> &str { + &self.localpart } /// Ask to set the displayname of the user. @@ -211,13 +211,13 @@ pub trait HomeserverConnection: Send + Sync { /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to query. + /// * `localpart` - The localpart of the user to query. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the user does not /// exist. - async fn query_user(&self, mxid: &str) -> Result; + async fn query_user(&self, localpart: &str) -> Result; /// Provision a user on the homeserver. /// @@ -247,16 +247,16 @@ pub trait HomeserverConnection: Send + Sync { /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to create a device for. + /// * `localpart` - The localpart of the user to create a device for. /// * `device_id` - The device ID to create. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the device could /// not be created. - async fn create_device( + async fn upsert_device( &self, - mxid: &str, + localpart: &str, device_id: &str, initial_display_name: Option<&str>, ) -> Result<(), anyhow::Error>; @@ -265,7 +265,7 @@ pub trait HomeserverConnection: Send + Sync { /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to update a device for. + /// * `localpart` - The localpart of the user to update a device for. /// * `device_id` - The device ID to update. /// * `display_name` - The new display name to set /// @@ -275,7 +275,7 @@ pub trait HomeserverConnection: Send + Sync { /// not be updated. async fn update_device_display_name( &self, - mxid: &str, + localpart: &str, device_id: &str, display_name: &str, ) -> Result<(), anyhow::Error>; @@ -284,90 +284,98 @@ pub trait HomeserverConnection: Send + Sync { /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to delete a device for. + /// * `localpart` - The localpart of the user to delete a device for. /// * `device_id` - The device ID to delete. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the device could /// not be deleted. - async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error>; + async fn delete_device(&self, localpart: &str, device_id: &str) -> Result<(), anyhow::Error>; /// Sync the list of devices of a user with the homeserver. /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to sync the devices for. + /// * `localpart` - The localpart of the user to sync the devices for. /// * `devices` - The list of devices to sync. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the devices could /// not be synced. - async fn sync_devices(&self, mxid: &str, devices: HashSet) - -> Result<(), anyhow::Error>; + async fn sync_devices( + &self, + localpart: &str, + devices: HashSet, + ) -> Result<(), anyhow::Error>; /// Delete a user on the homeserver. /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to delete. + /// * `localpart` - The localpart of the user to delete. /// * `erase` - Whether to ask the homeserver to erase the user's data. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the user could not /// be deleted. - async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), anyhow::Error>; + async fn delete_user(&self, localpart: &str, erase: bool) -> Result<(), anyhow::Error>; /// Reactivate a user on the homeserver. /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to reactivate. + /// * `localpart` - The localpart of the user to reactivate. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the user could not /// be reactivated. - async fn reactivate_user(&self, mxid: &str) -> Result<(), anyhow::Error>; + async fn reactivate_user(&self, localpart: &str) -> Result<(), anyhow::Error>; /// Set the displayname of a user on the homeserver. /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to set the displayname for. + /// * `localpart` - The localpart of the user to set the displayname for. /// * `displayname` - The displayname to set. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the displayname /// could not be set. - async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), anyhow::Error>; + async fn set_displayname( + &self, + localpart: &str, + displayname: &str, + ) -> Result<(), anyhow::Error>; /// Unset the displayname of a user on the homeserver. /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to unset the displayname for. + /// * `localpart` - The localpart of the user to unset the displayname for. /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the displayname /// could not be unset. - async fn unset_displayname(&self, mxid: &str) -> Result<(), anyhow::Error>; + async fn unset_displayname(&self, localpart: &str) -> Result<(), anyhow::Error>; /// Temporarily allow a user to reset their cross-signing keys. /// /// # Parameters /// - /// * `mxid` - The Matrix ID of the user to allow cross-signing key reset + /// * `localpart` - The localpart of the user to allow cross-signing key + /// reset /// /// # Errors /// /// Returns an error if the homeserver is unreachable or the cross-signing /// reset could not be allowed. - async fn allow_cross_signing_reset(&self, mxid: &str) -> Result<(), anyhow::Error>; + async fn allow_cross_signing_reset(&self, localpart: &str) -> Result<(), anyhow::Error>; } #[async_trait::async_trait] @@ -376,8 +384,8 @@ impl HomeserverConnection for &T (**self).homeserver() } - async fn query_user(&self, mxid: &str) -> Result { - (**self).query_user(mxid).await + async fn query_user(&self, localpart: &str) -> Result { + (**self).query_user(localpart).await } async fn provision_user(&self, request: &ProvisionRequest) -> Result { @@ -388,58 +396,62 @@ impl HomeserverConnection for &T (**self).is_localpart_available(localpart).await } - async fn create_device( + async fn upsert_device( &self, - mxid: &str, + localpart: &str, device_id: &str, initial_display_name: Option<&str>, ) -> Result<(), anyhow::Error> { (**self) - .create_device(mxid, device_id, initial_display_name) + .upsert_device(localpart, device_id, initial_display_name) .await } async fn update_device_display_name( &self, - mxid: &str, + localpart: &str, device_id: &str, display_name: &str, ) -> Result<(), anyhow::Error> { (**self) - .update_device_display_name(mxid, device_id, display_name) + .update_device_display_name(localpart, device_id, display_name) .await } - async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { - (**self).delete_device(mxid, device_id).await + async fn delete_device(&self, localpart: &str, device_id: &str) -> Result<(), anyhow::Error> { + (**self).delete_device(localpart, device_id).await } async fn sync_devices( &self, - mxid: &str, + localpart: &str, devices: HashSet, ) -> Result<(), anyhow::Error> { - (**self).sync_devices(mxid, devices).await + (**self).sync_devices(localpart, devices).await } - async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), anyhow::Error> { - (**self).delete_user(mxid, erase).await + async fn delete_user(&self, localpart: &str, erase: bool) -> Result<(), anyhow::Error> { + (**self).delete_user(localpart, erase).await } - async fn reactivate_user(&self, mxid: &str) -> Result<(), anyhow::Error> { - (**self).reactivate_user(mxid).await + async fn reactivate_user(&self, localpart: &str) -> Result<(), anyhow::Error> { + (**self).reactivate_user(localpart).await } - async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), anyhow::Error> { - (**self).set_displayname(mxid, displayname).await + async fn set_displayname( + &self, + localpart: &str, + displayname: &str, + ) -> Result<(), anyhow::Error> { + (**self).set_displayname(localpart, displayname).await } - async fn unset_displayname(&self, mxid: &str) -> Result<(), anyhow::Error> { - (**self).unset_displayname(mxid).await + async fn unset_displayname(&self, localpart: &str) -> Result<(), anyhow::Error> { + (**self).unset_displayname(localpart).await } - async fn allow_cross_signing_reset(&self, mxid: &str) -> Result<(), anyhow::Error> { - (**self).allow_cross_signing_reset(mxid).await + async fn allow_cross_signing_reset(&self, localpart: &str) -> Result<(), anyhow::Error> { + (**self).allow_cross_signing_reset(localpart).await } } @@ -450,8 +462,8 @@ impl HomeserverConnection for Arc { (**self).homeserver() } - async fn query_user(&self, mxid: &str) -> Result { - (**self).query_user(mxid).await + async fn query_user(&self, localpart: &str) -> Result { + (**self).query_user(localpart).await } async fn provision_user(&self, request: &ProvisionRequest) -> Result { @@ -462,57 +474,61 @@ impl HomeserverConnection for Arc { (**self).is_localpart_available(localpart).await } - async fn create_device( + async fn upsert_device( &self, - mxid: &str, + localpart: &str, device_id: &str, initial_display_name: Option<&str>, ) -> Result<(), anyhow::Error> { (**self) - .create_device(mxid, device_id, initial_display_name) + .upsert_device(localpart, device_id, initial_display_name) .await } async fn update_device_display_name( &self, - mxid: &str, + localpart: &str, device_id: &str, display_name: &str, ) -> Result<(), anyhow::Error> { (**self) - .update_device_display_name(mxid, device_id, display_name) + .update_device_display_name(localpart, device_id, display_name) .await } - async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { - (**self).delete_device(mxid, device_id).await + async fn delete_device(&self, localpart: &str, device_id: &str) -> Result<(), anyhow::Error> { + (**self).delete_device(localpart, device_id).await } async fn sync_devices( &self, - mxid: &str, + localpart: &str, devices: HashSet, ) -> Result<(), anyhow::Error> { - (**self).sync_devices(mxid, devices).await + (**self).sync_devices(localpart, devices).await } - async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), anyhow::Error> { - (**self).delete_user(mxid, erase).await + async fn delete_user(&self, localpart: &str, erase: bool) -> Result<(), anyhow::Error> { + (**self).delete_user(localpart, erase).await } - async fn reactivate_user(&self, mxid: &str) -> Result<(), anyhow::Error> { - (**self).reactivate_user(mxid).await + async fn reactivate_user(&self, localpart: &str) -> Result<(), anyhow::Error> { + (**self).reactivate_user(localpart).await } - async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), anyhow::Error> { - (**self).set_displayname(mxid, displayname).await + async fn set_displayname( + &self, + localpart: &str, + displayname: &str, + ) -> Result<(), anyhow::Error> { + (**self).set_displayname(localpart, displayname).await } - async fn unset_displayname(&self, mxid: &str) -> Result<(), anyhow::Error> { - (**self).unset_displayname(mxid).await + async fn unset_displayname(&self, localpart: &str) -> Result<(), anyhow::Error> { + (**self).unset_displayname(localpart).await } - async fn allow_cross_signing_reset(&self, mxid: &str) -> Result<(), anyhow::Error> { - (**self).allow_cross_signing_reset(mxid).await + async fn allow_cross_signing_reset(&self, localpart: &str) -> Result<(), anyhow::Error> { + (**self).allow_cross_signing_reset(localpart).await } } diff --git a/crates/matrix/src/mock.rs b/crates/matrix/src/mock.rs index 7969fee3c..0c315ff97 100644 --- a/crates/matrix/src/mock.rs +++ b/crates/matrix/src/mock.rs @@ -54,9 +54,10 @@ impl crate::HomeserverConnection for HomeserverConnection { &self.homeserver } - async fn query_user(&self, mxid: &str) -> Result { + async fn query_user(&self, localpart: &str) -> Result { + let mxid = self.mxid(localpart); let users = self.users.read().await; - let user = users.get(mxid).context("User not found")?; + let user = users.get(&mxid).context("User not found")?; Ok(MatrixUser { displayname: user.displayname.clone(), avatar_url: user.avatar_url.clone(), @@ -66,8 +67,9 @@ impl crate::HomeserverConnection for HomeserverConnection { async fn provision_user(&self, request: &ProvisionRequest) -> Result { let mut users = self.users.write().await; - let inserted = !users.contains_key(request.mxid()); - let user = users.entry(request.mxid().to_owned()).or_insert(MockUser { + let mxid = self.mxid(request.localpart()); + let inserted = !users.contains_key(&mxid); + let user = users.entry(mxid).or_insert(MockUser { sub: request.sub().to_owned(), avatar_url: None, displayname: None, @@ -107,51 +109,56 @@ impl crate::HomeserverConnection for HomeserverConnection { Ok(!users.contains_key(&mxid)) } - async fn create_device( + async fn upsert_device( &self, - mxid: &str, + localpart: &str, device_id: &str, _initial_display_name: Option<&str>, ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.devices.insert(device_id.to_owned()); Ok(()) } async fn update_device_display_name( &self, - mxid: &str, + localpart: &str, device_id: &str, _display_name: &str, ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.devices.get(device_id).context("Device not found")?; Ok(()) } - async fn delete_device(&self, mxid: &str, device_id: &str) -> Result<(), anyhow::Error> { + async fn delete_device(&self, localpart: &str, device_id: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.devices.remove(device_id); Ok(()) } async fn sync_devices( &self, - mxid: &str, + localpart: &str, devices: HashSet, ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.devices = devices; Ok(()) } - async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), anyhow::Error> { + async fn delete_user(&self, localpart: &str, erase: bool) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.devices.clear(); user.emails = None; user.deactivated = true; @@ -163,31 +170,39 @@ impl crate::HomeserverConnection for HomeserverConnection { Ok(()) } - async fn reactivate_user(&self, mxid: &str) -> Result<(), anyhow::Error> { + async fn reactivate_user(&self, localpart: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.deactivated = false; Ok(()) } - async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), anyhow::Error> { + async fn set_displayname( + &self, + localpart: &str, + displayname: &str, + ) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.displayname = Some(displayname.to_owned()); Ok(()) } - async fn unset_displayname(&self, mxid: &str) -> Result<(), anyhow::Error> { + async fn unset_displayname(&self, localpart: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.displayname = None; Ok(()) } - async fn allow_cross_signing_reset(&self, mxid: &str) -> Result<(), anyhow::Error> { + async fn allow_cross_signing_reset(&self, localpart: &str) -> Result<(), anyhow::Error> { + let mxid = self.mxid(localpart); let mut users = self.users.write().await; - let user = users.get_mut(mxid).context("User not found")?; + let user = users.get_mut(&mxid).context("User not found")?; user.cross_signing_reset_allowed = true; Ok(()) } @@ -207,11 +222,11 @@ mod tests { assert_eq!(conn.homeserver(), "example.org"); assert_eq!(conn.mxid("test"), mxid); - assert!(conn.query_user(mxid).await.is_err()); - assert!(conn.create_device(mxid, device, None).await.is_err()); - assert!(conn.delete_device(mxid, device).await.is_err()); + assert!(conn.query_user("test").await.is_err()); + assert!(conn.upsert_device("test", device, None).await.is_err()); + assert!(conn.delete_device("test", device).await.is_err()); - let request = ProvisionRequest::new("@test:example.org", "test") + let request = ProvisionRequest::new("test", "test") .set_displayname("Test User".into()) .set_avatar_url("mxc://example.org/1234567890".into()) .set_emails(vec!["test@example.org".to_owned()]); @@ -219,33 +234,33 @@ mod tests { let inserted = conn.provision_user(&request).await.unwrap(); assert!(inserted); - let user = conn.query_user(mxid).await.unwrap(); + let user = conn.query_user("test").await.unwrap(); assert_eq!(user.displayname, Some("Test User".into())); assert_eq!(user.avatar_url, Some("mxc://example.org/1234567890".into())); // Set the displayname again - assert!(conn.set_displayname(mxid, "John").await.is_ok()); + assert!(conn.set_displayname("test", "John").await.is_ok()); - let user = conn.query_user(mxid).await.unwrap(); + let user = conn.query_user("test").await.unwrap(); assert_eq!(user.displayname, Some("John".into())); // Unset the displayname - assert!(conn.unset_displayname(mxid).await.is_ok()); + assert!(conn.unset_displayname("test").await.is_ok()); - let user = conn.query_user(mxid).await.unwrap(); + let user = conn.query_user("test").await.unwrap(); assert_eq!(user.displayname, None); // Deleting a non-existent device should not fail - assert!(conn.delete_device(mxid, device).await.is_ok()); + assert!(conn.delete_device("test", device).await.is_ok()); // Create the device - assert!(conn.create_device(mxid, device, None).await.is_ok()); + assert!(conn.upsert_device("test", device, None).await.is_ok()); // Create the same device again - assert!(conn.create_device(mxid, device, None).await.is_ok()); + assert!(conn.upsert_device("test", device, None).await.is_ok()); // XXX: there is no API to query devices yet in the trait // Delete the device - assert!(conn.delete_device(mxid, device).await.is_ok()); + assert!(conn.delete_device("test", device).await.is_ok()); // The user we just created should be not available assert!(!conn.is_localpart_available("test").await.unwrap()); diff --git a/crates/matrix/src/readonly.rs b/crates/matrix/src/readonly.rs index 10a0642c2..2efa935f9 100644 --- a/crates/matrix/src/readonly.rs +++ b/crates/matrix/src/readonly.rs @@ -28,8 +28,8 @@ impl HomeserverConnection for ReadOnlyHomeserverConnect self.inner.homeserver() } - async fn query_user(&self, mxid: &str) -> Result { - self.inner.query_user(mxid).await + async fn query_user(&self, localpart: &str) -> Result { + self.inner.query_user(localpart).await } async fn provision_user(&self, _request: &ProvisionRequest) -> Result { @@ -40,9 +40,9 @@ impl HomeserverConnection for ReadOnlyHomeserverConnect self.inner.is_localpart_available(localpart).await } - async fn create_device( + async fn upsert_device( &self, - _mxid: &str, + _localpart: &str, _device_id: &str, _initial_display_name: Option<&str>, ) -> Result<(), anyhow::Error> { @@ -51,42 +51,46 @@ impl HomeserverConnection for ReadOnlyHomeserverConnect async fn update_device_display_name( &self, - _mxid: &str, + _localpart: &str, _device_id: &str, _display_name: &str, ) -> Result<(), anyhow::Error> { anyhow::bail!("Device display name update is not supported in read-only mode"); } - async fn delete_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> { + async fn delete_device(&self, _localpart: &str, _device_id: &str) -> Result<(), anyhow::Error> { anyhow::bail!("Device deletion is not supported in read-only mode"); } async fn sync_devices( &self, - _mxid: &str, + _localpart: &str, _devices: HashSet, ) -> Result<(), anyhow::Error> { anyhow::bail!("Device synchronization is not supported in read-only mode"); } - async fn delete_user(&self, _mxid: &str, _erase: bool) -> Result<(), anyhow::Error> { + async fn delete_user(&self, _localpart: &str, _erase: bool) -> Result<(), anyhow::Error> { anyhow::bail!("User deletion is not supported in read-only mode"); } - async fn reactivate_user(&self, _mxid: &str) -> Result<(), anyhow::Error> { + async fn reactivate_user(&self, _localpart: &str) -> Result<(), anyhow::Error> { anyhow::bail!("User reactivation is not supported in read-only mode"); } - async fn set_displayname(&self, _mxid: &str, _displayname: &str) -> Result<(), anyhow::Error> { + async fn set_displayname( + &self, + _localpart: &str, + _displayname: &str, + ) -> Result<(), anyhow::Error> { anyhow::bail!("User displayname update is not supported in read-only mode"); } - async fn unset_displayname(&self, _mxid: &str) -> Result<(), anyhow::Error> { + async fn unset_displayname(&self, _localpart: &str) -> Result<(), anyhow::Error> { anyhow::bail!("User displayname update is not supported in read-only mode"); } - async fn allow_cross_signing_reset(&self, _mxid: &str) -> Result<(), anyhow::Error> { + async fn allow_cross_signing_reset(&self, _localpart: &str) -> Result<(), anyhow::Error> { anyhow::bail!("Allowing cross-signing reset is not supported in read-only mode"); } } diff --git a/crates/tasks/src/matrix.rs b/crates/tasks/src/matrix.rs index 92e15f448..4855a4bf1 100644 --- a/crates/tasks/src/matrix.rs +++ b/crates/tasks/src/matrix.rs @@ -51,7 +51,6 @@ impl RunnableJob for ProvisionUserJob { .context("User not found") .map_err(JobError::fail)?; - let mxid = matrix.mxid(&user.username); let emails = repo .user_email() .all(&user) @@ -60,7 +59,8 @@ impl RunnableJob for ProvisionUserJob { .into_iter() .map(|email| email.email) .collect(); - let mut request = ProvisionRequest::new(mxid.clone(), user.sub.clone()).set_emails(emails); + let mut request = + ProvisionRequest::new(user.username.clone(), user.sub.clone()).set_emails(emails); if let Some(display_name) = self.display_name_to_set() { request = request.set_displayname(display_name.to_owned()); @@ -71,6 +71,7 @@ impl RunnableJob for ProvisionUserJob { .await .map_err(JobError::retry)?; + let mxid = matrix.mxid(&user.username); if created { info!(%user.id, %mxid, "User created"); } else { @@ -241,9 +242,8 @@ impl RunnableJob for SyncDevicesJob { } } - let mxid = matrix.mxid(&user.username); matrix - .sync_devices(&mxid, devices) + .sync_devices(&user.username, devices) .await .map_err(JobError::retry)?; diff --git a/deny.toml b/deny.toml index 62f8dab87..333ad892b 100644 --- a/deny.toml +++ b/deny.toml @@ -19,10 +19,6 @@ ignore = [ # RSA key extraction "Marvin Attack". This is only relevant when using # PKCS#1 v1.5 encryption, which we don't "RUSTSEC-2023-0071", - - # `paste`, as used by `aws-lc-rs` is unmaintained, but we're not concerned - # about it having a security vulnerability - "RUSTSEC-2024-0436", ] [licenses] @@ -73,11 +69,13 @@ skip = [ { name = "thiserror-impl", version = "1.0.69" }, # axum-macros, sqlx-macros and sea-query-attr use an old version { name = "heck", version = "0.4.1" }, - # wasmtime -> cranelift is depending on this old version - { name = "itertools", version = "0.12.1" }, # pad depends on an old version { name = "unicode-width", version = "0.1.14" }, { name = "zerocopy", version = "0.7.35" }, # hashbrown 0.14.5 depends on this old version + # opa-wasm -> wasmtime -> memfd depends on this old version + # https://github.com/lucab/memfd-rs/pull/72 + { name = "rustix", version = "0.38.44" }, + { name = "linux-raw-sys", version = "0.9.4" }, # We are still mainly using rand 0.8 { name = "rand", version = "0.8.5" }, diff --git a/docs/config.schema.json b/docs/config.schema.json index 14af56e7a..8b726f1e9 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1707,18 +1707,32 @@ "description": "The kind of homeserver it is.", "oneOf": [ { - "description": "Homeserver is Synapse", + "description": "Homeserver is Synapse, using the legacy API\n\nThis will switch to using the modern API in a few releases.", "type": "string", "enum": [ "synapse" ] }, { - "description": "Homeserver is Synapse, in read-only mode\n\nThis is meant for testing rolling out Matrix Authentication Service with no risk of writing data to the homeserver.", + "description": "Homeserver is Synapse, using the legacy API, in read-only mode\n\nThis is meant for testing rolling out Matrix Authentication Service with no risk of writing data to the homeserver.\n\nThis will switch to using the modern API in a few releases.", "type": "string", "enum": [ "synapse_read_only" ] + }, + { + "description": "Homeserver is Synapse, using the legacy API,", + "type": "string", + "enum": [ + "synapse_legacy" + ] + }, + { + "description": "Homeserver is Synapse, with the modern API available", + "type": "string", + "enum": [ + "synapse_modern" + ] } ] },