diff --git a/.cargo/config.toml b/.cargo/config.toml index 307bcfe43..18f6fb282 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,8 @@ +# 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. + [build] rustflags = ["--cfg", "tokio_unstable"] diff --git a/.codecov.yml b/.codecov.yml index 54f4aaf72..a946b3cea 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,3 +1,8 @@ +# 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. + comment: false flag_management: diff --git a/.config/nextest.toml b/.config/nextest.toml index 351fb92d7..7ed06faa5 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,2 +1,7 @@ +# 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. + [profile.default] retries = 1 diff --git a/.dockerignore b/.dockerignore index e016faae8..01fcc861a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,8 @@ +# 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. + target/ crates/*/target crates/*/node_modules diff --git a/.editorconfig b/.editorconfig index 23e144851..ccbe6ed4b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,8 @@ +# 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. + root = true [*] diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 7e11055a9..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.wasm binary diff --git a/.github/actions/build-frontend/action.yml b/.github/actions/build-frontend/action.yml index a7a1fe2c5..417f1f9ae 100644 --- a/.github/actions/build-frontend/action.yml +++ b/.github/actions/build-frontend/action.yml @@ -1,3 +1,8 @@ +# 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. + name: Build the frontend assets description: Installs Node.js and builds the frontend assets from the frontend directory @@ -7,7 +12,7 @@ runs: - name: Install Node uses: actions/setup-node@v4.2.0 with: - node-version: '22' + node-version: "22" - name: Install dependencies run: npm ci diff --git a/.github/actions/build-policies/action.yml b/.github/actions/build-policies/action.yml index 274aa8134..0eba08e6e 100644 --- a/.github/actions/build-policies/action.yml +++ b/.github/actions/build-policies/action.yml @@ -1,3 +1,8 @@ +# 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. + name: Build the Open Policy Agent policies description: Installs OPA and builds the policies diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a98d07e09..8b67a1414 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,3 +1,8 @@ +# 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. + version: 2 updates: - package-ecosystem: "cargo" diff --git a/.github/release.yml b/.github/release.yml index 8b25ed964..3633ae68a 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,3 +1,8 @@ +# 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. + changelog: categories: - title: Bug Fixes diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore index 504afef81..efa4841d7 100644 --- a/.github/scripts/.gitignore +++ b/.github/scripts/.gitignore @@ -1,2 +1,7 @@ +# 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. + node_modules/ package-lock.json diff --git a/.github/scripts/cleanup-pr.cjs b/.github/scripts/cleanup-pr.cjs index e5189cec8..43db85727 100644 --- a/.github/scripts/cleanup-pr.cjs +++ b/.github/scripts/cleanup-pr.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/scripts/commit-and-tag.cjs b/.github/scripts/commit-and-tag.cjs index b95782541..086e1b83e 100644 --- a/.github/scripts/commit-and-tag.cjs +++ b/.github/scripts/commit-and-tag.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/scripts/create-release-branch.cjs b/.github/scripts/create-release-branch.cjs index c7c0018a3..2508765ff 100644 --- a/.github/scripts/create-release-branch.cjs +++ b/.github/scripts/create-release-branch.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/scripts/create-version-tag.cjs b/.github/scripts/create-version-tag.cjs index 97536c5ff..47e00ecb1 100644 --- a/.github/scripts/create-version-tag.cjs +++ b/.github/scripts/create-version-tag.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/scripts/merge-back.cjs b/.github/scripts/merge-back.cjs index 30b08329d..d3948398e 100644 --- a/.github/scripts/merge-back.cjs +++ b/.github/scripts/merge-back.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/scripts/update-release-branch.cjs b/.github/scripts/update-release-branch.cjs index 0a94aa217..78dbb4686 100644 --- a/.github/scripts/update-release-branch.cjs +++ b/.github/scripts/update-release-branch.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/scripts/update-unstable-tag.cjs b/.github/scripts/update-unstable-tag.cjs index 1958adcec..765e85d6a 100644 --- a/.github/scripts/update-unstable-tag.cjs +++ b/.github/scripts/update-unstable-tag.cjs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @ts-check diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ac2669e5..574529ef7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,3 +1,8 @@ +# 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. + name: Build on: @@ -221,7 +226,7 @@ jobs: steps: - name: Docker meta id: meta - uses: docker/metadata-action@v5.7.0 + uses: docker/metadata-action@v5.8.0 with: images: "${{ env.IMAGE }}" bake-target: docker-metadata-action @@ -237,7 +242,7 @@ jobs: - name: Docker meta (debug variant) id: meta-debug - uses: docker/metadata-action@v5.7.0 + uses: docker/metadata-action@v5.8.0 with: images: "${{ env.IMAGE }}" bake-target: docker-metadata-action-debug @@ -253,10 +258,10 @@ jobs: type=sha - name: Setup Cosign - uses: sigstore/cosign-installer@v3.8.2 + uses: sigstore/cosign-installer@v3.9.2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.1 with: buildkitd-config-inline: | [registry."docker.io"] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b3f438f5f..f290e7796 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,3 +1,8 @@ +# 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. + name: CI on: @@ -153,7 +158,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Run `cargo-deny` - uses: EmbarkStudios/cargo-deny-action@v2.0.11 + uses: EmbarkStudios/cargo-deny-action@v2.0.12 with: rust-version: stable @@ -210,7 +215,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.86.0 + uses: dtolnay/rust-toolchain@1.87.0 with: components: clippy diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 12f2419b3..ca3860aaa 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -1,3 +1,8 @@ +# 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. + name: Coverage on: diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 811a35b9b..84170931e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,10 +1,15 @@ +# 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. + name: Build and deploy the documentation on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/merge-back.yaml b/.github/workflows/merge-back.yaml index 04884e2be..5cb8ebdf1 100644 --- a/.github/workflows/merge-back.yaml +++ b/.github/workflows/merge-back.yaml @@ -1,3 +1,8 @@ +# 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. + name: Merge back a reference to main on: workflow_call: diff --git a/.github/workflows/release-branch.yaml b/.github/workflows/release-branch.yaml index 8575d5975..72f24c4af 100644 --- a/.github/workflows/release-branch.yaml +++ b/.github/workflows/release-branch.yaml @@ -1,3 +1,8 @@ +# 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. + name: Create a new release branch on: workflow_dispatch: diff --git a/.github/workflows/release-bump.yaml b/.github/workflows/release-bump.yaml index 841893d50..3b147a2ce 100644 --- a/.github/workflows/release-bump.yaml +++ b/.github/workflows/release-bump.yaml @@ -1,3 +1,8 @@ +# 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. + name: Bump the version on a release branch on: workflow_dispatch: diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index 02555f5e1..631221145 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -1,3 +1,8 @@ +# 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. + name: Tag a new version on: workflow_call: diff --git a/.github/workflows/translations-download.yaml b/.github/workflows/translations-download.yaml index d2380c0df..130a76d38 100644 --- a/.github/workflows/translations-download.yaml +++ b/.github/workflows/translations-download.yaml @@ -1,3 +1,8 @@ +# 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. + name: Download translation files from Localazy on: workflow_dispatch: diff --git a/.github/workflows/translations-upload.yaml b/.github/workflows/translations-upload.yaml index 2c5a27c3e..9fbe53c00 100644 --- a/.github/workflows/translations-upload.yaml +++ b/.github/workflows/translations-upload.yaml @@ -1,3 +1,8 @@ +# 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. + name: Upload translation files to Localazy on: push: diff --git a/.gitignore b/.gitignore index 1046cc35a..d98402c23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ +# 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. + # Rust -target/ +target # Editors .idea diff --git a/.rustfmt.toml b/.rustfmt.toml index 0e4510bbb..72a97f569 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,3 +1,8 @@ +# 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. + max_width = 100 comment_width = 80 wrap_comments = true diff --git a/Cargo.lock b/Cargo.lock index 6dd7c2c7e..eaf3132a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,9 +23,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -64,14 +64,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -95,8 +95,8 @@ dependencies = [ "bytes", "cfg-if", "http", - "indexmap 2.9.0", - "schemars", + "indexmap 2.10.0", + "schemars 0.8.22", "serde", "serde_json", "serde_qs", @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -155,36 +155,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -271,14 +271,15 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", + "pin-project-lite", "slab", ] @@ -288,7 +289,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", "async-io", "async-lock", @@ -315,7 +316,7 @@ dependencies = [ "futures-timer", "futures-util", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "mime", "multer", "num-traits", @@ -367,16 +368,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_json", ] [[package]] name = "async-io" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ "async-lock", "cfg-if", @@ -385,10 +386,9 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.0.8", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -404,11 +404,11 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-io", "async-lock", "async-signal", @@ -417,15 +417,14 @@ dependencies = [ "cfg-if", "event-listener 5.4.0", "futures-lite", - "rustix", - "tracing", + "rustix 1.0.8", ] [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" dependencies = [ "async-io", "async-lock", @@ -433,10 +432,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 1.0.8", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -516,9 +515,9 @@ dependencies = [ [[package]] name = "atomic" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" dependencies = [ "bytemuck", ] @@ -531,15 +530,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-sys", "zeroize", @@ -547,9 +546,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ "bindgen", "cc", @@ -649,9 +648,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -688,7 +687,7 @@ checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" dependencies = [ "base64", "blowfish", - "getrandom 0.3.2", + "getrandom 0.3.3", "subtle", "zeroize", ] @@ -769,11 +768,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -792,18 +791,18 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" dependencies = [ "allocator-api2", ] [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -841,9 +840,9 @@ dependencies = [ [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] @@ -859,9 +858,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.18" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -885,15 +884,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chacha20" @@ -936,25 +929,14 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", - "chrono-tz-build", "phf", ] -[[package]] -name = "chrono-tz-build" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" -dependencies = [ - "parse-zoneinfo", - "phf_codegen", -] - [[package]] name = "chronoutil" version = "0.2.7" @@ -998,9 +980,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" dependencies = [ "clap_builder", "clap_derive", @@ -1008,9 +990,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" dependencies = [ "anstream", "anstyle", @@ -1020,9 +1002,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1032,9 +1014,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" @@ -1047,15 +1029,18 @@ dependencies = [ [[package]] name = "cobs" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -1099,7 +1084,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width 0.2.1", "windows-sys 0.59.0", ] @@ -1137,9 +1122,9 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" dependencies = [ "cookie", "document-features", @@ -1154,9 +1139,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1188,33 +1173,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4b56ebe316895d3fa37775d0a87b0c889cc933f5c8b253dbcc7c7bcb7fe7e4" +checksum = "0ae7b60ec3fd7162427d3b3801520a1908bef7c035b52983cd3ca11b8e7deb51" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cabbc01dfbd7dcd6c329ca44f0212910309c221797ac736a67a5bc8857fe1b" +checksum = "6511c200fed36452697b4b6b161eae57d917a2044e6333b1c1389ed63ccadeee" +dependencies = [ + "cranelift-srcgen", +] [[package]] name = "cranelift-bforest" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ffe46df300a45f1dc6f609dc808ce963f0e3a2e971682c479a2d13e3b9b8ef" +checksum = "5f7086a645aa58bae979312f64e3029ac760ac1b577f5cd2417844842a2ca07f" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b265bed7c51e1921fdae6419791d31af77d33662ee56d7b0fa0704dc8d231cab" +checksum = "5225b4dec45f3f3dbf383f12560fac5ce8d780f399893607e21406e12e77f491" dependencies = [ "serde", "serde_derive", @@ -1222,9 +1210,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e606230a7e3a6897d603761baee0d19f88d077f17b996bb5089488a29ae96e41" +checksum = "858fb3331e53492a95979378d6df5208dd1d0d315f19c052be8115f4efc888e0" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1236,7 +1224,7 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "log", "pulley-interpreter", "regalloc2", @@ -1244,39 +1232,41 @@ dependencies = [ "serde", "smallvec", "target-lexicon", + "wasmtime-internal-math", ] [[package]] name = "cranelift-codegen-meta" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a63bffafc23bc60969ad528e138788495999d935f0adcfd6543cb151ca8637d" +checksum = "456715b9d5f12398f156d5081096e7b5d039f01b9ecc49790a011c8e43e65b5f" 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.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af50281b67324b58e843170a6a5943cf6d387c06f7eeacc9f5696e4ab7ae7d7e" +checksum = "0306041099499833f167a0ddb707e1e54100f1a84eab5631bc3dad249708f482" [[package]] name = "cranelift-control" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c20c1b38d1abfbcebb0032e497e71156c0e3b8dcb3f0a92b9863b7bcaec290c" +checksum = "1672945e1f9afc2297f49c92623f5eabc64398e2cb0d824f8f72a2db2df5af23" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2c67d95507c51b4a1ff3f3555fe4bfec36b9e13c1b684ccc602736f5d5f4a2" +checksum = "aa3cd55eb5f3825b9ae5de1530887907360a6334caccdc124c52f6d75246c98a" dependencies = [ "cranelift-bitset", "serde", @@ -1285,9 +1275,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e002691cc69c38b54fc7ec93e5be5b744f627d027031d991cc845d1d512d0ce" +checksum = "781f9905f8139b8de22987b66b522b416fe63eb76d823f0b3a8c02c8fd9500c7" dependencies = [ "cranelift-codegen", "log", @@ -1297,21 +1287,27 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93588ed1796cbcb0e2ad160403509e2c5d330d80dd6e0014ac6774c7ebac496" +checksum = "a05337a2b02c3df00b4dd9a263a027a07b3dff49f61f7da3b5d195c21eaa633d" [[package]] name = "cranelift-native" -version = "0.118.0" +version = "0.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b09bdd6407bf5d89661b80cf926ce731c9e8cc184bf49102267a2369a8358e" +checksum = "2eee7a496dd66380082c9c5b6f2d5fa149cec0ec383feec5caf079ca2b3671c2" dependencies = [ "cranelift-codegen", "libc", "target-lexicon", ] +[[package]] +name = "cranelift-srcgen" +version = "0.122.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b530783809a55cb68d070e0de60cfbb3db0dc94c8850dd5725411422bedcf6bb" + [[package]] name = "crc" version = "3.3.0" @@ -1329,9 +1325,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1344,7 +1340,7 @@ checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" dependencies = [ "chrono", "once_cell", - "winnow", + "winnow 0.6.26", ] [[package]] @@ -1631,20 +1627,20 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "duration-str" -version = "0.12.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ad6b66883f70e2f38f1ee99e3797b9d7e7b7fb051ed2e23e027c81753056c8" +checksum = "eb333721800c025e363e902b293040778f8ac79913db4f013abf1f1d7d382fd7" dependencies = [ "rust_decimal", "thiserror 2.0.12", - "winnow", + "winnow 0.7.12", ] [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" @@ -1692,9 +1688,9 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b9cde6a71f9f758440470f3de16db6c09a02c443ce66850d87f5410548fb8e" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" dependencies = [ "base64", "memchr", @@ -1741,12 +1737,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2026,29 +2022,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", ] [[package]] @@ -2068,7 +2060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" dependencies = [ "fallible-iterator", - "indexmap 2.9.0", + "indexmap 2.10.0", "stable_deref_trait", ] @@ -2101,7 +2093,7 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "nonzero_ext", "parking_lot", "portable-atomic", @@ -2124,9 +2116,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -2134,7 +2126,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2159,9 +2151,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -2175,7 +2167,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] @@ -2216,15 +2208,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -2261,13 +2247,13 @@ dependencies = [ [[package]] name = "hostname" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows", + "windows-link", ] [[package]] @@ -2361,9 +2347,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -2395,7 +2381,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core", ] [[package]] @@ -2746,12 +2732,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "serde", ] @@ -2800,6 +2786,17 @@ dependencies = [ "similar", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2812,7 +2809,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -2893,7 +2890,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -2993,9 +2990,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lettre" -version = "0.11.15" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bc2b8eabb6a30b235d6f716f7f36479f4b38cbe65b8747aefee51f89e8437" +checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" dependencies = [ "async-std", "async-trait", @@ -3014,35 +3011,35 @@ dependencies = [ "percent-encoding", "quoted_printable", "rustls", + "rustls-platform-verifier", "socket2", "tokio", "tokio-rustls", "tracing", "url", - "webpki-roots", ] [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libsqlite3-sys" @@ -3060,6 +3057,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" @@ -3079,15 +3082,15 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "litrs" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -3104,16 +3107,16 @@ dependencies = [ [[package]] name = "mach2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] [[package]] name = "mas-axum-utils" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "axum", @@ -3147,7 +3150,7 @@ dependencies = [ [[package]] name = "mas-cli" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "axum", @@ -3221,7 +3224,7 @@ dependencies = [ [[package]] name = "mas-config" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "camino", @@ -3241,7 +3244,7 @@ dependencies = [ "rand_chacha 0.3.1", "rustls-pemfile", "rustls-pki-types", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_with", @@ -3253,7 +3256,7 @@ dependencies = [ [[package]] name = "mas-context" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "console", "opentelemetry", @@ -3269,7 +3272,7 @@ dependencies = [ [[package]] name = "mas-data-model" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "base64ct", "chrono", @@ -3290,7 +3293,7 @@ dependencies = [ [[package]] name = "mas-email" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "async-trait", "lettre", @@ -3301,7 +3304,7 @@ dependencies = [ [[package]] name = "mas-handlers" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "aide", "anyhow", @@ -3323,13 +3326,14 @@ dependencies = [ "hex", "hyper", "icu_normalizer", - "indexmap 2.9.0", + "indexmap 2.10.0", "insta", "lettre", "mas-axum-utils", "mas-config", "mas-context", "mas-data-model", + "mas-email", "mas-http", "mas-i18n", "mas-iana", @@ -3341,6 +3345,7 @@ dependencies = [ "mas-router", "mas-storage", "mas-storage-pg", + "mas-tasks", "mas-templates", "mime", "minijinja", @@ -3355,7 +3360,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest", "rustls", - "schemars", + "schemars 0.8.22", "sentry", "serde", "serde_json", @@ -3379,7 +3384,7 @@ dependencies = [ [[package]] name = "mas-http" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "futures-util", "headers", @@ -3400,7 +3405,7 @@ dependencies = [ [[package]] name = "mas-i18n" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "camino", "icu_calendar", @@ -3422,7 +3427,7 @@ dependencies = [ [[package]] name = "mas-i18n-scan" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "camino", "clap", @@ -3436,15 +3441,15 @@ dependencies = [ [[package]] name = "mas-iana" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] [[package]] name = "mas-iana-codegen" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "async-trait", @@ -3460,7 +3465,7 @@ dependencies = [ [[package]] name = "mas-jose" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "base64ct", "chrono", @@ -3477,7 +3482,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rsa", - "schemars", + "schemars 0.8.22", "sec1", "serde", "serde_json", @@ -3490,7 +3495,7 @@ dependencies = [ [[package]] name = "mas-keystore" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "aead", "base64ct", @@ -3518,7 +3523,7 @@ dependencies = [ [[package]] name = "mas-listener" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "bytes", @@ -3543,7 +3548,7 @@ dependencies = [ [[package]] name = "mas-matrix" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "async-trait", @@ -3553,7 +3558,7 @@ dependencies = [ [[package]] name = "mas-matrix-synapse" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "async-trait", @@ -3570,7 +3575,7 @@ dependencies = [ [[package]] name = "mas-oidc-client" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "assert_matches", "async-trait", @@ -3606,14 +3611,14 @@ dependencies = [ [[package]] name = "mas-policy" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "arc-swap", "mas-data-model", "oauth2-types", "opa-wasm", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 2.0.12", @@ -3623,7 +3628,7 @@ dependencies = [ [[package]] name = "mas-router" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "axum", "serde", @@ -3634,7 +3639,7 @@ dependencies = [ [[package]] name = "mas-spa" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "camino", "serde", @@ -3643,7 +3648,7 @@ dependencies = [ [[package]] name = "mas-storage" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "async-trait", "chrono", @@ -3665,7 +3670,7 @@ dependencies = [ [[package]] name = "mas-storage-pg" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "async-trait", "chrono", @@ -3692,7 +3697,7 @@ dependencies = [ [[package]] name = "mas-tasks" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "async-trait", @@ -3724,7 +3729,7 @@ dependencies = [ [[package]] name = "mas-templates" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "arc-swap", @@ -3754,7 +3759,7 @@ dependencies = [ [[package]] name = "mas-tower" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "http", "opentelemetry", @@ -3793,9 +3798,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memfd" @@ -3803,7 +3808,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix", + "rustix 0.38.44", ] [[package]] @@ -3830,9 +3835,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.10.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd72e8b4e42274540edabec853f607c015c73436159b06c39c7af85a20433155" +checksum = "4e60ac08614cc09062820e51d5d94c2fce16b94ea4e5003bb81b99a95f84e876" dependencies = [ "memo-map", "self_cell", @@ -3843,9 +3848,9 @@ dependencies = [ [[package]] name = "minijinja-contrib" -version = "2.10.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457f85f9c4c5b17d11fcf9bbe7c0dbba64843c5ee040005956f1a510b6679fe2" +checksum = "f93e5bfa889f16d8c10ec92ac964074a68a7206c0fd9748ff23a31942c85d97c" dependencies = [ "minijinja", "serde", @@ -3859,22 +3864,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -4005,11 +4010,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -4024,12 +4029,12 @@ dependencies = [ [[package]] name = "oauth2-types" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "assert_matches", "base64ct", "chrono", - "indexmap 2.9.0", + "indexmap 2.10.0", "insta", "language-tags", "mas-iana", @@ -4049,8 +4054,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "crc32fast", - "hashbrown 0.15.2", - "indexmap 2.9.0", + "hashbrown 0.15.4", + "indexmap 2.10.0", "memchr", ] @@ -4061,10 +4066,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "opa-wasm" -version = "0.1.5" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c07ec35ceaacb13349669e772705036975bde72b612e72b26a6bd6a71d909" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "opa-wasm" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd2bab45ae1b87f45b4ddea74902158543322dc49bf45d2f714c50bbf8cf44f" dependencies = [ "anyhow", "base64", @@ -4110,9 +4121,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "opentelemetry" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" dependencies = [ "futures-core", "futures-sink", @@ -4124,35 +4135,32 @@ dependencies = [ [[package]] name = "opentelemetry-http" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" dependencies = [ "async-trait", "bytes", "http", "opentelemetry", "reqwest", - "tracing", ] [[package]] name = "opentelemetry-jaeger-propagator" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae2eb16c37705755c3e09332bebdcac9b37ca1539b3ac2d2f43a154401514ae" +checksum = "090b8ec07bb2e304b529581aa1fe530d7861298c9ef549ebbf44a4a56472c539" dependencies = [ "opentelemetry", - "tracing", ] [[package]] name = "opentelemetry-otlp" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" dependencies = [ - "futures-core", "http", "opentelemetry", "opentelemetry-http", @@ -4165,8 +4173,7 @@ dependencies = [ [[package]] name = "opentelemetry-prometheus" version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098a71a4430bb712be6130ed777335d2e5b19bc8566de5f2edddfce906def6ab" +source = "git+https://github.com/sandhose/opentelemetry-rust.git?branch=otel-prometheus-0.30#193906c7577b4f8ee642aa771191c7d80b14a297" dependencies = [ "once_cell", "opentelemetry", @@ -4177,9 +4184,9 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -4189,9 +4196,9 @@ dependencies = [ [[package]] name = "opentelemetry-resource-detectors" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1622068e1c522685d4ec468d60f9d25dc3bc8714e699315dc42488b72e2194" +checksum = "0a44e076f07fa3d76e741991f4f7d3ecbac0eed8521ced491fbdf8db77d024cf" dependencies = [ "opentelemetry", "opentelemetry-semantic-conventions", @@ -4200,49 +4207,47 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b29a9f89f1a954936d5aa92f19b2feec3c8f3971d3e96206640db7f9706ae3" +checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" [[package]] name = "opentelemetry-stdout" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e27d446dabd68610ef0b77d07b102ecde827a4596ea9c01a4d3811e945b286" +checksum = "447191061af41c3943e082ea359ab8b64ff27d6d34d30d327df309ddef1eef6f" dependencies = [ "chrono", - "futures-util", "opentelemetry", "opentelemetry_sdk", ] [[package]] name = "opentelemetry_sdk" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "glob", "opentelemetry", "percent-encoding", - "rand 0.9.0", + "rand 0.9.2", "serde_json", "thiserror 2.0.12", "tokio", "tokio-stream", - "tracing", ] [[package]] name = "os_info" -version = "3.10.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e53c24761286860eba4e2c8b23a0161526476b1de520139d69cdb85a6b5" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" dependencies = [ "log", + "plist", "serde", "windows-sys 0.52.0", ] @@ -4294,9 +4299,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -4304,9 +4309,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -4321,15 +4326,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -4341,12 +4337,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" @@ -4400,9 +4390,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -4411,9 +4401,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -4421,9 +4411,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", @@ -4434,49 +4424,28 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] [[package]] name = "phf" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ "phf_shared", ] -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" dependencies = [ "siphasher", ] @@ -4569,18 +4538,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "polling" -version = "3.7.4" +name = "plist" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap 2.10.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -4608,15 +4589,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "postcard" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -4636,14 +4617,14 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn", @@ -4660,18 +4641,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -4749,9 +4730,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.120" +version = "2.1.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc02eae5cf5475af3fde3792e68ff74eb8625638fcaa8f1ffb91b7b98bfae4a5" +checksum = "7ccc42254cd72c304dd475038409a3a2d7d9b9e906e9fec65458278d3bfdc8d3" dependencies = [ "psl-types", ] @@ -4764,22 +4745,34 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" dependencies = [ "cc", ] [[package]] name = "pulley-interpreter" -version = "31.0.0" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3325791708ad50580aeacfcce06cb5e462c9ba7a2368e109cb2012b944b70e" +checksum = "b89c4319786b16c1a6a38ee04788d32c669b61ba4b69da2162c868c18be99c1b" dependencies = [ "cranelift-bitset", "log", - "wasmtime-math", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938543690519c20c3a480d20a8efcc8e69abeb44093ab1df4e7c1f81f26c677a" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4792,63 +4785,18 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] [[package]] -name = "quinn" -version = "0.11.7" +name = "quick-xml" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" -dependencies = [ - "bytes", - "getrandom 0.3.2", - "rand 0.9.0", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.12", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", + "memchr", ] [[package]] @@ -4868,9 +4816,9 @@ checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -4885,13 +4833,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", ] [[package]] @@ -4920,7 +4867,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4929,7 +4876,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -4963,22 +4910,42 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] -name = "regalloc2" -version = "0.11.2" +name = "ref-cast" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "log", "rustc-hash 2.1.1", "smallvec", @@ -5030,9 +4997,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -5052,7 +5019,6 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "serde", @@ -5088,7 +5054,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -5116,15 +5082,15 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b75da013b362664c3e161662902e5da3f77e990525681b59c6035bac27e87b4" +checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf" dependencies = [ "as_variant", "base64", "bytes", "form_urlencoded", - "indexmap 2.9.0", + "indexmap 2.10.0", "js_int", "percent-encoding", "regex", @@ -5153,9 +5119,9 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1182e83ee5cd10121974f163337b16af68a93eedfc7cdbdbd52307ac7e1d743" +checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c" dependencies = [ "cfg-if", "proc-macro-crate", @@ -5169,9 +5135,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.37.1" +version = "1.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" dependencies = [ "arrayvec", "num-traits", @@ -5179,9 +5145,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -5213,20 +5179,32 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] -name = "rustls" -version = "0.23.27" +name = "rustix" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -5260,15 +5238,14 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "web-time", "zeroize", ] [[package]] name = "rustls-platform-verifier" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +checksum = "eda84358ed17f1f354cf4b1909ad346e6c7bc2513e8c40eb08e0157aa13a9070" dependencies = [ "core-foundation", "core-foundation-sys", @@ -5293,9 +5270,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.2" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -5305,9 +5282,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -5351,13 +5328,37 @@ dependencies = [ "chrono", "dyn-clone", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "schemars_derive", "serde", "serde_json", "url", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -5473,9 +5474,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" @@ -5485,9 +5486,9 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "sentry" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255914a8e53822abd946e2ce8baa41d4cded6b8e938913b7f7b9da5b7ab44335" +checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030" dependencies = [ "httpdate", "reqwest", @@ -5502,21 +5503,20 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00293cd332a859961f24fd69258f7e92af736feaeb91020cff84dac4188a4302" +checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653" dependencies = [ "backtrace", - "once_cell", "regex", "sentry-core", ] [[package]] name = "sentry-contexts" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961990f9caa76476c481de130ada05614cd7f5aa70fb57c2142f0e09ad3fb2aa" +checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88" dependencies = [ "hostname", "libc", @@ -5528,22 +5528,22 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a6409d845707d82415c800290a5d63be5e3df3c2e417b0997c60531dfbd35ef" +checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc" dependencies = [ - "once_cell", - "rand 0.8.5", + "rand 0.9.2", "sentry-types", "serde", "serde_json", + "url", ] [[package]] name = "sentry-panic" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609b1a12340495ce17baeec9e08ff8ed423c337c1a84dffae36a178c783623f3" +checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe" dependencies = [ "sentry-backtrace", "sentry-core", @@ -5551,9 +5551,9 @@ dependencies = [ [[package]] name = "sentry-tower" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b98005537e38ee3bc10e7d36e7febe9b8e573d03f2ddd85fcdf05d21f9abd6d" +checksum = "4a303d0127d95ae928a937dcc0886931d28b4186e7338eea7d5786827b69b002" dependencies = [ "axum", "http", @@ -5566,10 +5566,11 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f4e86402d5c50239dc7d8fd3f6d5e048221d5fcb4e026d8d50ab57fe4644cb" +checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080" dependencies = [ + "bitflags", "sentry-backtrace", "sentry-core", "tracing-core", @@ -5578,16 +5579,16 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.37.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3f117b8755dbede8260952de2aeb029e20f432e72634e8969af34324591631" +checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412" dependencies = [ "debugid", "hex", - "rand 0.8.5", + "rand 0.9.2", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "url", "uuid", @@ -5631,7 +5632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" dependencies = [ "form_urlencoded", - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -5639,11 +5640,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -5675,9 +5676,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -5696,15 +5697,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -5714,9 +5717,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", @@ -5730,7 +5733,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -5782,9 +5785,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -5813,30 +5816,27 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5869,19 +5869,13 @@ dependencies = [ [[package]] name = "sprintf" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46781e6f401f1557f5b4560284baf7268bd9ca531e9e387120a8695fe5bc1fb1" +checksum = "78222247fc55e10208ed1ba60f8296390bc67a489bc27a36231765d8d6f60ec5" dependencies = [ - "thiserror 1.0.69", + "thiserror 2.0.12", ] -[[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" @@ -5912,9 +5906,9 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "hashlink", - "indexmap 2.9.0", + "indexmap 2.10.0", "ipnetwork", "log", "memchr", @@ -5931,7 +5925,7 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] @@ -6090,9 +6084,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" dependencies = [ "cc", "cfg-if", @@ -6160,9 +6154,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -6171,7 +6165,7 @@ dependencies = [ [[package]] name = "syn2mas" -version = "0.17.0-rc.0" +version = "0.20.0" dependencies = [ "anyhow", "arc-swap", @@ -6216,9 +6210,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -6233,15 +6227,14 @@ 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.3", "once_cell", - "rustix", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -6274,19 +6267,19 @@ dependencies = [ [[package]] name = "thiserror-ext" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4323942237f7cc071061f2c5f0db919e6053c2cdf58c6bc974883073429737" +checksum = "5fb7e61141f4141832ca9aad63c3c90023843f944a1975460abdacc64d03f534" dependencies = [ - "thiserror 1.0.69", + "thiserror 2.0.12", "thiserror-ext-derive", ] [[package]] name = "thiserror-ext-derive" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96541747c50e6c73e094737938f4f5dfaf50c48a31adff4197a3e2a481371674" +checksum = "2b5042dd3b562d1d57711be902006a0003fa2781b81d5b2bec07416be31586ff" dependencies = [ "either", "proc-macro2", @@ -6318,12 +6311,11 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -6386,20 +6378,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6457,16 +6451,16 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "0.8.19" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -6476,31 +6470,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.7.12", ] [[package]] name = "tonic" -version = "0.12.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", "base64", @@ -6599,9 +6593,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -6610,9 +6604,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -6643,9 +6637,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" dependencies = [ "js-sys", "once_cell", @@ -6781,9 +6775,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -6949,9 +6943,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -7041,9 +7035,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.226.0" +version = "0.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d81b727619aec227dce83e7f7420d4e56c79acd044642a356ea045b98d4e13" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" dependencies = [ "leb128fmt", "wasmparser", @@ -7051,22 +7045,22 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.226.0" +version = "0.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc28600dcb2ba68d7e5f1c3ba4195c2bddc918c0243fd702d0b6dbd05689b681" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" dependencies = [ "bitflags", - "hashbrown 0.15.2", - "indexmap 2.9.0", + "hashbrown 0.15.4", + "indexmap 2.10.0", "semver", "serde", ] [[package]] name = "wasmprinter" -version = "0.226.0" +version = "0.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "753a0516fa6c01756ee861f36878dfd9875f273aea9409d9ea390a333c5bcdc2" +checksum = "75aa8e9076de6b9544e6dab4badada518cca0bf4966d35b131bbd057aed8fa0a" dependencies = [ "anyhow", "termcolor", @@ -7075,9 +7069,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "31.0.0" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fe78033c72da8741e724d763daf1375c93a38bfcea99c873ee4415f6098c3f" +checksum = "b6fe976922a16af3b0d67172c473d1fd4f1aa5d0af9c8ba6538c741f3af686f4" dependencies = [ "addr2line", "anyhow", @@ -7086,106 +7080,48 @@ dependencies = [ "bumpalo", "cc", "cfg-if", - "hashbrown 0.15.2", - "indexmap 2.9.0", + "hashbrown 0.15.4", + "indexmap 2.10.0", "libc", "log", "mach2", "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", - "wasmtime-asm-macros", - "wasmtime-component-macro", - "wasmtime-cranelift", "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-icache-coherence", - "wasmtime-math", - "wasmtime-slab", - "wasmtime-versioned-export-macros", + "wasmtime-internal-asm-macros", + "wasmtime-internal-component-macro", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", "windows-sys 0.59.0", ] -[[package]] -name = "wasmtime-asm-macros" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f3d44ae977d70ccf80938b371d5ec60b6adedf60800b9e8dd1223bb69f4cbc" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "wasmtime-component-macro" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397e68ee29eb072d8d8741c9d2c971a284cd1bc960ebf2c1f6a33ea6ba16d6e1" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser", -] - -[[package]] -name = "wasmtime-component-util" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f292ef5eb2cf3d414c2bde59c7fa0feeba799c8db9a8c5a656ad1d1a1d05e10b" - -[[package]] -name = "wasmtime-cranelift" -version = "31.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fc12eb8ea695a30007a4849a5fd56209dd86a15579e92e0c27c27122818505" -dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli", - "itertools 0.12.1", - "log", - "object", - "pulley-interpreter", - "smallvec", - "target-lexicon", - "thiserror 1.0.69", - "wasmparser", - "wasmtime-environ", - "wasmtime-versioned-export-macros", -] - [[package]] name = "wasmtime-environ" -version = "31.0.0" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6b4bf08e371edf262cccb62de10e214bd4aaafaa069f1cd49c9c1c3a5ae8e4" +checksum = "44b6264a78d806924abbc76bbc75eac24976bc83bdfb938e5074ae551242436f" dependencies = [ "anyhow", "cranelift-bitset", "cranelift-entity", "gimli", - "indexmap 2.9.0", + "indexmap 2.10.0", "log", "object", "postcard", @@ -7199,25 +7135,83 @@ dependencies = [ ] [[package]] -name = "wasmtime-fiber" -version = "31.0.0" +name = "wasmtime-internal-asm-macros" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8828d7d8fbe90d087a9edea9223315caf7eb434848896667e5d27889f1173" +checksum = "6775a9b516559716e5710e95a8014ca0adcc81e5bf4d3ad7899d89ae40094d1a" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3d098205e405e6b5ced06c1815621b823464b6ea289eaafe494139b0aee287" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219252067216242ed2b32665611b0ee356d6e92cbb897ecb9a10cae0b97bdeca" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec9ad7565e6a8de7cb95484e230ff689db74a4a085219e0da0cbd637a29c01c" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.12", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b636ff8b220ebaf29dfe3b23770e4b2bad317b9683e3bf7345e162387385b39" dependencies = [ "anyhow", "cc", "cfg-if", - "rustix", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", + "libc", + "rustix 1.0.8", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", "windows-sys 0.59.0", ] [[package]] -name = "wasmtime-jit-icache-coherence" -version = "31.0.0" +name = "wasmtime-internal-jit-icache-coherence" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54f6c6c7e9d7eeee32dfcc10db7f29d505ee7dd28d00593ea241d5f70698e64" +checksum = "4417e06b7f80baff87d9770852c757a39b8d7f11d78b2620ca992b8725f16f50" dependencies = [ "anyhow", "cfg-if", @@ -7226,25 +7220,38 @@ dependencies = [ ] [[package]] -name = "wasmtime-math" -version = "31.0.0" +name = "wasmtime-internal-math" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1108aad2e6965698f9207ea79b80eda2b3dcc57dcb69f4258296d4664ae32cd" +checksum = "7710d5c4ecdaa772927fd11e5dc30a9a62d1fc8fe933e11ad5576ad596ab6612" dependencies = [ "libm", ] [[package]] -name = "wasmtime-slab" -version = "31.0.0" +name = "wasmtime-internal-slab" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d6a321317281b721c5530ef733e8596ecc6065035f286ccd155b3fa8e0ab2f" +checksum = "e6ab22fabe1eed27ab01fd47cd89deacf43ad222ed7fd169ba6f4dd1fbddc53b" [[package]] -name = "wasmtime-versioned-export-macros" -version = "31.0.0" +name = "wasmtime-internal-unwinder" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5732a5c86efce7bca121a61d8c07875f6b85c1607aa86753b40f7f8bd9d3a780" +checksum = "307708f302f5dcf19c1bbbfb3d9f2cbc837dd18088a7988747b043a46ba38ecc" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "35.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342b0466f92b7217a4de9e114175fedee1907028567d2548bcd42f71a8b5b016" dependencies = [ "proc-macro2", "quote", @@ -7252,14 +7259,14 @@ dependencies = [ ] [[package]] -name = "wasmtime-wit-bindgen" -version = "31.0.0" +name = "wasmtime-internal-wit-bindgen" +version = "35.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505c13fa0cac6c43e805347acf1e916c8de54e3790f2c22873c5692964b09b62" +checksum = "1ae057d44a5b60e6ec529b0c21809a9d1fc92e91ef6e0f6771ed11dd02a94a08" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.9.0", + "indexmap 2.10.0", "wit-parser", ] @@ -7285,18 +7292,27 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "0.26.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -7310,7 +7326,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.44", ] [[package]] @@ -7360,30 +7376,11 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", @@ -7416,24 +7413,24 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -7474,6 +7471,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -7513,13 +7519,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -7538,6 +7561,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -7556,6 +7585,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -7574,12 +7609,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -7598,6 +7645,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -7616,6 +7669,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -7634,6 +7693,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -7652,6 +7717,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.6.26" @@ -7662,10 +7733,19 @@ dependencies = [ ] [[package]] -name = "wiremock" -version = "0.6.3" +name = "winnow" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "101681b74cd87b5899e87bcf5a64e83334dd313fcd3053ea72e6dba18928e301" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wiremock" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" dependencies = [ "assert-json-diff", "async-trait", @@ -7696,13 +7776,13 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.226.0" +version = "0.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f007722bfd43a2978c5b8b90f02c927dddf0f11c5f5b50929816b3358718cd" +checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a" dependencies = [ "anyhow", "id-arena", - "indexmap 2.9.0", + "indexmap 2.10.0", "log", "semver", "serde", @@ -7769,38 +7849,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 68f867595..33717660b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,16 @@ +# 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. + [workspace] default-members = ["crates/cli"] members = ["crates/*"] resolver = "2" # Updated in the CI with a `sed` command -package.version = "0.17.0-rc.0" -package.license = "AGPL-3.0-only" +package.version = "0.20.0" +package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial" package.authors = ["Element Backend Team"] package.edition = "2024" package.homepage = "https://element-hq.github.io/matrix-authentication-service/" @@ -21,6 +26,7 @@ all = { level = "deny", priority = -1 } pedantic = { level = "warn", priority = -1 } str_to_string = "deny" +too_many_lines = "allow" [workspace.lints.rustdoc] broken_intra_doc_links = "deny" @@ -28,35 +34,35 @@ broken_intra_doc_links = "deny" [workspace.dependencies] # Workspace crates -mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.17.0-rc.0" } -mas-cli = { path = "./crates/cli/", version = "=0.17.0-rc.0" } -mas-config = { path = "./crates/config/", version = "=0.17.0-rc.0" } -mas-context = { path = "./crates/context/", version = "=0.17.0-rc.0" } -mas-data-model = { path = "./crates/data-model/", version = "=0.17.0-rc.0" } -mas-email = { path = "./crates/email/", version = "=0.17.0-rc.0" } -mas-graphql = { path = "./crates/graphql/", version = "=0.17.0-rc.0" } -mas-handlers = { path = "./crates/handlers/", version = "=0.17.0-rc.0" } -mas-http = { path = "./crates/http/", version = "=0.17.0-rc.0" } -mas-i18n = { path = "./crates/i18n/", version = "=0.17.0-rc.0" } -mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.17.0-rc.0" } -mas-iana = { path = "./crates/iana/", version = "=0.17.0-rc.0" } -mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.17.0-rc.0" } -mas-jose = { path = "./crates/jose/", version = "=0.17.0-rc.0" } -mas-keystore = { path = "./crates/keystore/", version = "=0.17.0-rc.0" } -mas-listener = { path = "./crates/listener/", version = "=0.17.0-rc.0" } -mas-matrix = { path = "./crates/matrix/", version = "=0.17.0-rc.0" } -mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.17.0-rc.0" } -mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.17.0-rc.0" } -mas-policy = { path = "./crates/policy/", version = "=0.17.0-rc.0" } -mas-router = { path = "./crates/router/", version = "=0.17.0-rc.0" } -mas-spa = { path = "./crates/spa/", version = "=0.17.0-rc.0" } -mas-storage = { path = "./crates/storage/", version = "=0.17.0-rc.0" } -mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.17.0-rc.0" } -mas-tasks = { path = "./crates/tasks/", version = "=0.17.0-rc.0" } -mas-templates = { path = "./crates/templates/", version = "=0.17.0-rc.0" } -mas-tower = { path = "./crates/tower/", version = "=0.17.0-rc.0" } -oauth2-types = { path = "./crates/oauth2-types/", version = "=0.17.0-rc.0" } -syn2mas = { path = "./crates/syn2mas", version = "=0.17.0-rc.0" } +mas-axum-utils = { path = "./crates/axum-utils/", version = "=0.20.0" } +mas-cli = { path = "./crates/cli/", version = "=0.20.0" } +mas-config = { path = "./crates/config/", version = "=0.20.0" } +mas-context = { path = "./crates/context/", version = "=0.20.0" } +mas-data-model = { path = "./crates/data-model/", version = "=0.20.0" } +mas-email = { path = "./crates/email/", version = "=0.20.0" } +mas-graphql = { path = "./crates/graphql/", version = "=0.20.0" } +mas-handlers = { path = "./crates/handlers/", version = "=0.20.0" } +mas-http = { path = "./crates/http/", version = "=0.20.0" } +mas-i18n = { path = "./crates/i18n/", version = "=0.20.0" } +mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=0.20.0" } +mas-iana = { path = "./crates/iana/", version = "=0.20.0" } +mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=0.20.0" } +mas-jose = { path = "./crates/jose/", version = "=0.20.0" } +mas-keystore = { path = "./crates/keystore/", version = "=0.20.0" } +mas-listener = { path = "./crates/listener/", version = "=0.20.0" } +mas-matrix = { path = "./crates/matrix/", version = "=0.20.0" } +mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=0.20.0" } +mas-oidc-client = { path = "./crates/oidc-client/", version = "=0.20.0" } +mas-policy = { path = "./crates/policy/", version = "=0.20.0" } +mas-router = { path = "./crates/router/", version = "=0.20.0" } +mas-spa = { path = "./crates/spa/", version = "=0.20.0" } +mas-storage = { path = "./crates/storage/", version = "=0.20.0" } +mas-storage-pg = { path = "./crates/storage-pg/", version = "=0.20.0" } +mas-tasks = { path = "./crates/tasks/", version = "=0.20.0" } +mas-templates = { path = "./crates/templates/", version = "=0.20.0" } +mas-tower = { path = "./crates/tower/", version = "=0.20.0" } +oauth2-types = { path = "./crates/oauth2-types/", version = "=0.20.0" } +syn2mas = { path = "./crates/syn2mas", version = "=0.20.0" } # OpenAPI schema generation and validation [workspace.dependencies.aide] @@ -149,7 +155,7 @@ version = "0.15.11" # Cookie store [workspace.dependencies.cookie_store] -version = "0.21.1" +version = "0.22.0" default-features = false features = ["serde_json"] @@ -161,7 +167,7 @@ features = ["serde", "clock"] # CLI argument parsing [workspace.dependencies.clap] -version = "4.5.40" +version = "4.5.42" features = ["derive"] # Object Identifiers (OIDs) as constants @@ -268,7 +274,7 @@ features = ["client", "server", "http1", "http2"] # Additional Hyper utilties [workspace.dependencies.hyper-util] -version = "0.1.14" +version = "0.1.16" features = [ "client", "server", @@ -315,7 +321,7 @@ features = ["std"] # HashMap which preserves insertion order [workspace.dependencies.indexmap] -version = "2.9.0" +version = "2.10.0" features = ["serde"] # Indented string literals @@ -348,10 +354,12 @@ features = ["serde"] # Email sending [workspace.dependencies.lettre] -version = "0.11.15" +version = "0.11.18" default-features = false features = [ - "tokio1-rustls-tls", + "tokio1-rustls", + "rustls-platform-verifier", + "aws-lc-rs", "hostname", "builder", "tracing", @@ -370,12 +378,12 @@ version = "0.3.17" # Templates [workspace.dependencies.minijinja] -version = "2.10.2" +version = "2.11.0" features = ["loader", "json", "speedups", "unstable_machinery"] # Additional filters for minijinja [workspace.dependencies.minijinja-contrib] -version = "2.10.2" +version = "2.11.0" features = ["pycompat"] # Utilities to deal with non-zero values @@ -384,40 +392,42 @@ version = "0.3.0" # Open Policy Agent support through WASM [workspace.dependencies.opa-wasm] -version = "0.1.5" +version = "0.1.7" # OpenTelemetry [workspace.dependencies.opentelemetry] -version = "0.29.1" +version = "0.30.0" features = ["trace", "metrics"] [workspace.dependencies.opentelemetry-http] -version = "0.29.0" +version = "0.30.0" features = ["reqwest"] [workspace.dependencies.opentelemetry-jaeger-propagator] -version = "0.29.0" +version = "0.30.0" [workspace.dependencies.opentelemetry-otlp] -version = "0.29.0" +version = "0.30.0" default-features = false features = ["trace", "metrics", "http-proto"] [workspace.dependencies.opentelemetry-prometheus] -version = "0.29.1" +# https://github.com/open-telemetry/opentelemetry-rust/pull/3076 +git = "https://github.com/sandhose/opentelemetry-rust.git" +branch = "otel-prometheus-0.30" [workspace.dependencies.opentelemetry-resource-detectors] -version = "0.8.0" +version = "0.9.0" [workspace.dependencies.opentelemetry-semantic-conventions] -version = "0.29.0" +version = "0.30.0" features = ["semconv_experimental"] [workspace.dependencies.opentelemetry-stdout] -version = "0.29.0" +version = "0.30.0" features = ["trace", "metrics"] [workspace.dependencies.opentelemetry_sdk] -version = "0.29.0" +version = "0.30.0" features = [ "experimental_trace_batch_span_processor_with_async_runtime", "experimental_metrics_periodicreader_with_async_runtime", "rt-tokio", ] [workspace.dependencies.tracing-opentelemetry] -version = "0.30.0" +version = "0.31.0" default-features = false # P256 elliptic curve @@ -446,11 +456,11 @@ features = ["std"] # Parser generator [workspace.dependencies.pest] -version = "2.8.0" +version = "2.8.1" # Pest derive macros [workspace.dependencies.pest_derive] -version = "2.8.0" +version = "2.8.1" # Pin projection [workspace.dependencies.pin-project-lite] @@ -468,7 +478,7 @@ features = ["std", "pkcs5", "encryption"] # Public Suffix List [workspace.dependencies.psl] -version = "2.1.120" +version = "2.1.127" # Prometheus metrics [workspace.dependencies.prometheus] @@ -492,9 +502,15 @@ version = "1.11.1" # High-level HTTP client [workspace.dependencies.reqwest] -version = "0.12.20" +version = "0.12.22" default-features = false -features = ["http2", "rustls-tls-manual-roots", "charset", "json", "socks"] +features = [ + "http2", + "rustls-tls-manual-roots-no-provider", + "charset", + "json", + "socks", +] # RSA cryptography [workspace.dependencies.rsa] @@ -507,11 +523,11 @@ version = "2.1.1" # Matrix-related types [workspace.dependencies.ruma-common] -version = "0.15.2" +version = "0.15.4" # TLS stack [workspace.dependencies.rustls] -version = "0.23.27" +version = "0.23.31" # PEM parsing for rustls [workspace.dependencies.rustls-pemfile] @@ -523,7 +539,7 @@ version = "1.12.0" # Use platform-specific verifier for TLS [workspace.dependencies.rustls-platform-verifier] -version = "0.5.3" +version = "0.6.0" # systemd service status notification [workspace.dependencies.sd-notify] @@ -557,18 +573,18 @@ features = [ # Sentry error tracking [workspace.dependencies.sentry] -version = "0.37.0" +version = "0.42.0" default-features = false features = ["backtrace", "contexts", "panic", "tower", "reqwest"] # Sentry tower layer [workspace.dependencies.sentry-tower] -version = "0.37.0" +version = "0.42.0" features = ["http", "axum-matched-path"] # Sentry tracing integration [workspace.dependencies.sentry-tracing] -version = "0.37.0" +version = "0.42.0" # Serialization and deserialization [workspace.dependencies.serde] @@ -577,7 +593,7 @@ features = ["derive"] # Most of the time, if we need serde, we need derive # JSON serialization and deserialization [workspace.dependencies.serde_json] -version = "1.0.140" +version = "1.0.142" features = ["preserve_order"] # URL encoded form serialization @@ -586,7 +602,7 @@ version = "0.7.1" # Custom serialization helpers [workspace.dependencies.serde_with] -version = "3.12.0" +version = "3.14.0" features = ["hex", "chrono"] # YAML serialization @@ -604,7 +620,7 @@ version = "2.2.0" # Low-level socket manipulation [workspace.dependencies.socket2] -version = "0.5.10" +version = "0.6.0" # Subject Public Key Info [workspace.dependencies.spki] @@ -630,11 +646,11 @@ features = [ version = "2.0.12" [workspace.dependencies.thiserror-ext] -version = "0.2.1" +version = "0.3.0" # Async runtime [workspace.dependencies.tokio] -version = "1.45.1" +version = "1.47.1" features = ["full"] [workspace.dependencies.tokio-stream] @@ -713,7 +729,7 @@ version = "2.5.0" # HTTP mock server [workspace.dependencies.wiremock] -version = "0.6.3" +version = "0.6.4" # User-agent parser [workspace.dependencies.woothee] diff --git a/Dockerfile b/Dockerfile index 085504530..1c1c66b56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,8 @@ # syntax = docker/dockerfile:1.7.1 +# 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. # Builds a minimal image with the binary only. It is multi-arch capable, # cross-building to aarch64 and x86_64. When cross-compiling, Docker sets two @@ -8,7 +12,7 @@ # The Debian version and version name must be in sync ARG DEBIAN_VERSION=12 ARG DEBIAN_VERSION_NAME=bookworm -ARG RUSTC_VERSION=1.86.0 +ARG RUSTC_VERSION=1.87.0 ARG NODEJS_VERSION=20.15.0 ARG OPA_VERSION=1.1.0 ARG CARGO_AUDITABLE_VERSION=0.6.6 diff --git a/LICENSE-COMMERCIAL b/LICENSE-COMMERCIAL new file mode 100644 index 000000000..173e03e0c --- /dev/null +++ b/LICENSE-COMMERCIAL @@ -0,0 +1,6 @@ +Licensees holding a valid commercial license with Element may use this +software in accordance with the terms contained in a written agreement +between you and Element. + +To purchase a commercial license please contact our sales team at +licensing@element.io diff --git a/README.md b/README.md index 8afa6d439..8bd75b2a9 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,15 @@ Anyone can contribute to translations through [Localazy](https://localazy.com/el ## 🏗️ Contributing See the [contribution guidelines](https://element-hq.github.io/matrix-authentication-service/development/contributing.html) for information on how to contribute to this project. + +## ⚖️ Copyright & License + +Copyright 2024, 2025 New Vector Ltd. +Copyright 2021-2024 The Matrix.org Foundation C.I.C. + +This software is dual-licensed by New Vector Ltd (Element). It can be used either: + +(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR + +(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to). +Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses. diff --git a/biome.json b/biome.json index 8800caa30..9dc190833 100644 --- a/biome.json +++ b/biome.json @@ -1,28 +1,27 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "organizeImports": { - "enabled": true - }, + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { - "ignore": [ - ".devcontainer/**", - "docs/**", - "translations/**", - "policies/**", - "crates/**", - "frontend/package.json", - "frontend/src/gql/**", - "frontend/src/routeTree.gen.ts", - "frontend/.storybook/locales.ts", - "frontend/.storybook/public/mockServiceWorker.js", - "frontend/locales/*.json", - "**/coverage/**", - "**/dist/**" + "includes": [ + "**", + "!**/.devcontainer/**", + "!**/docs/**", + "!**/translations/**", + "!**/policies/**", + "!**/crates/**", + "!**/frontend/package.json", + "!**/frontend/src/gql/**", + "!**/frontend/src/routeTree.gen.ts", + "!**/frontend/.storybook/locales.ts", + "!**/frontend/.storybook/public/mockServiceWorker.js", + "!**/frontend/locales/**/*.json", + "!**/coverage/**", + "!**/dist/**" ] }, "formatter": { @@ -36,6 +35,19 @@ "correctness": { "noUnusedImports": "warn", "noUnusedVariables": "warn" + }, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error", + "noDescendingSpecificity": "off" } } } diff --git a/book.toml b/book.toml index e21f37eca..88f5e8263 100644 --- a/book.toml +++ b/book.toml @@ -1,3 +1,8 @@ +# 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. + # Documentation for possible options in this file is at # https://rust-lang.github.io/mdBook/format/config.html [book] diff --git a/clippy.toml b/clippy.toml index 3cbf7c74c..41d584369 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,3 +1,8 @@ +# 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. + doc-valid-idents = ["OpenID", "OAuth", "..", "PostgreSQL", "SQLite"] disallowed-methods = [ @@ -10,7 +15,6 @@ disallowed-methods = [ ] disallowed-types = [ - "rand::OsRng", { path = "std::path::PathBuf", reason = "use camino::Utf8PathBuf instead" }, { path = "std::path::Path", reason = "use camino::Utf8Path instead" }, ] diff --git a/crates/axum-utils/Cargo.toml b/crates/axum-utils/Cargo.toml index 6084984cd..ceb47a4c7 100644 --- a/crates/axum-utils/Cargo.toml +++ b/crates/axum-utils/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-axum-utils" version.workspace = true diff --git a/crates/axum-utils/src/client_authorization.rs b/crates/axum-utils/src/client_authorization.rs index 19d6c9e7a..65d885853 100644 --- a/crates/axum-utils/src/client_authorization.rs +++ b/crates/axum-utils/src/client_authorization.rs @@ -1,21 +1,20 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; use axum::{ BoxError, Json, extract::{ - Form, FromRequest, FromRequestParts, + Form, FromRequest, rejection::{FailedToDeserializeForm, FormRejection}, }, response::IntoResponse, }; -use axum_extra::typed_header::{TypedHeader, TypedHeaderRejectionReason}; -use headers::{Authorization, authorization::Basic}; +use headers::authorization::{Basic, Bearer, Credentials as _}; use http::{Request, StatusCode}; use mas_data_model::{Client, JwksOrJwksUri}; use mas_http::RequestBuilderExt; @@ -60,17 +59,30 @@ pub enum Credentials { client_id: String, jwt: Box>>, }, + BearerToken { + token: String, + }, } impl Credentials { /// Get the `client_id` of the credentials #[must_use] - pub fn client_id(&self) -> &str { + pub fn client_id(&self) -> Option<&str> { match self { Credentials::None { client_id } | Credentials::ClientSecretBasic { client_id, .. } | Credentials::ClientSecretPost { client_id, .. } - | Credentials::ClientAssertionJwtBearer { client_id, .. } => client_id, + | Credentials::ClientAssertionJwtBearer { client_id, .. } => Some(client_id), + Credentials::BearerToken { .. } => None, + } + } + + /// Get the bearer token from the credentials. + #[must_use] + pub fn bearer_token(&self) -> Option<&str> { + match self { + Credentials::BearerToken { token } => Some(token), + _ => None, } } @@ -89,6 +101,7 @@ impl Credentials { | Credentials::ClientSecretBasic { client_id, .. } | Credentials::ClientSecretPost { client_id, .. } | Credentials::ClientAssertionJwtBearer { client_id, .. } => client_id, + Credentials::BearerToken { .. } => return Ok(None), }; repo.oauth2_client().find_by_client_id(client_id).await @@ -239,7 +252,7 @@ pub struct ClientAuthorization { impl ClientAuthorization { /// Get the `client_id` from the credentials. #[must_use] - pub fn client_id(&self) -> &str { + pub fn client_id(&self) -> Option<&str> { self.credentials.client_id() } } @@ -355,31 +368,41 @@ where { type Rejection = ClientAuthorizationError; - #[allow(clippy::too_many_lines)] async fn from_request( req: Request, state: &S, ) -> Result { - // Split the request into parts so we can extract some headers - let (mut parts, body) = req.into_parts(); + enum Authorization { + Basic(String, String), + Bearer(String), + } - let header = - TypedHeader::>::from_request_parts(&mut parts, state).await; + // Sadly, the typed-header 'Authorization' doesn't let us check for both + // Basic and Bearer at the same time, so we need to parse them manually + let authorization = if let Some(header) = req.headers().get(http::header::AUTHORIZATION) { + let bytes = header.as_bytes(); + if bytes.len() >= 6 && bytes[..6].eq_ignore_ascii_case(b"Basic ") { + let Some(decoded) = Basic::decode(header) else { + return Err(ClientAuthorizationError::InvalidHeader); + }; - // Take the Authorization header - let credentials_from_header = match header { - Ok(header) => Some((header.username().to_owned(), header.password().to_owned())), - Err(err) => match err.reason() { - // If it's missing it is fine - TypedHeaderRejectionReason::Missing => None, - // If the header could not be parsed, return the error - _ => return Err(ClientAuthorizationError::InvalidHeader), - }, + Some(Authorization::Basic( + decoded.username().to_owned(), + decoded.password().to_owned(), + )) + } else if bytes.len() >= 7 && bytes[..7].eq_ignore_ascii_case(b"Bearer ") { + let Some(decoded) = Bearer::decode(header) else { + return Err(ClientAuthorizationError::InvalidHeader); + }; + + Some(Authorization::Bearer(decoded.token().to_owned())) + } else { + return Err(ClientAuthorizationError::InvalidHeader); + } + } else { + None }; - // Reconstruct the request from the parts - let req = Request::from_parts(parts, body); - // Take the form value let ( client_id_from_form, @@ -407,13 +430,19 @@ where // And now, figure out the actual auth method let credentials = match ( - credentials_from_header, + authorization, client_id_from_form, client_secret_from_form, client_assertion_type, client_assertion, ) { - (Some((client_id, client_secret)), client_id_from_form, None, None, None) => { + ( + Some(Authorization::Basic(client_id, client_secret)), + client_id_from_form, + None, + None, + None, + ) => { if let Some(client_id_from_form) = client_id_from_form { // If the client_id was in the body, verify it matches with the header if client_id != client_id_from_form { @@ -483,6 +512,11 @@ where }); } + (Some(Authorization::Bearer(token)), None, None, None, None) => { + // Got a bearer token + Credentials::BearerToken { token } + } + (None, None, None, None, None) => { // Special case when there are no credentials anywhere return Err(ClientAuthorizationError::MissingCredentials); @@ -677,4 +711,29 @@ mod tests { jwt.verify_with_shared_secret(b"client-secret".to_vec()) .unwrap(); } + + #[tokio::test] + async fn bearer_token_test() { + let req = Request::builder() + .method(Method::POST) + .header( + http::header::CONTENT_TYPE, + mime::APPLICATION_WWW_FORM_URLENCODED.as_ref(), + ) + .header(http::header::AUTHORIZATION, "Bearer token") + .body(Body::new("foo=bar".to_owned())) + .unwrap(); + + assert_eq!( + ClientAuthorization::::from_request(req, &()) + .await + .unwrap(), + ClientAuthorization { + credentials: Credentials::BearerToken { + token: "token".to_owned(), + }, + form: Some(serde_json::json!({"foo": "bar"})), + } + ); + } } diff --git a/crates/axum-utils/src/cookies.rs b/crates/axum-utils/src/cookies.rs index c3572a266..97f1db830 100644 --- a/crates/axum-utils/src/cookies.rs +++ b/crates/axum-utils/src/cookies.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Private (encrypted) cookie jar, based on axum-extra's cookie jar diff --git a/crates/axum-utils/src/csrf.rs b/crates/axum-utils/src/csrf.rs index bf94e4ce9..677a81062 100644 --- a/crates/axum-utils/src/csrf.rs +++ b/crates/axum-utils/src/csrf.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use base64ct::{Base64UrlUnpadded, Encoding}; use chrono::{DateTime, Duration, Utc}; diff --git a/crates/axum-utils/src/error_wrapper.rs b/crates/axum-utils/src/error_wrapper.rs index 2bfd448fc..0865768d6 100644 --- a/crates/axum-utils/src/error_wrapper.rs +++ b/crates/axum-utils/src/error_wrapper.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::response::{IntoResponse, Response}; diff --git a/crates/axum-utils/src/fancy_error.rs b/crates/axum-utils/src/fancy_error.rs index 98c2a3c51..cb6d4e5eb 100644 --- a/crates/axum-utils/src/fancy_error.rs +++ b/crates/axum-utils/src/fancy_error.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ Extension, diff --git a/crates/axum-utils/src/jwt.rs b/crates/axum-utils/src/jwt.rs index 14e966c0d..f7747828c 100644 --- a/crates/axum-utils/src/jwt.rs +++ b/crates/axum-utils/src/jwt.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::response::{IntoResponse, Response}; use axum_extra::typed_header::TypedHeader; diff --git a/crates/axum-utils/src/language_detection.rs b/crates/axum-utils/src/language_detection.rs index efb148f74..057453a0a 100644 --- a/crates/axum-utils/src/language_detection.rs +++ b/crates/axum-utils/src/language_detection.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::cmp::Reverse; diff --git a/crates/axum-utils/src/lib.rs b/crates/axum-utils/src/lib.rs index a3dc31cca..a4e769dcd 100644 --- a/crates/axum-utils/src/lib.rs +++ b/crates/axum-utils/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(clippy::future_not_send)] #![allow(clippy::module_name_repetitions)] diff --git a/crates/axum-utils/src/sentry.rs b/crates/axum-utils/src/sentry.rs index 2744accff..9cf26301e 100644 --- a/crates/axum-utils/src/sentry.rs +++ b/crates/axum-utils/src/sentry.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::convert::Infallible; diff --git a/crates/axum-utils/src/session.rs b/crates/axum-utils/src/session.rs index 98cbd4865..b5ed670ae 100644 --- a/crates/axum-utils/src/session.rs +++ b/crates/axum-utils/src/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_data_model::BrowserSession; use mas_storage::RepositoryAccess; diff --git a/crates/axum-utils/src/user_authorization.rs b/crates/axum-utils/src/user_authorization.rs index 52e901c5c..fb6d233c0 100644 --- a/crates/axum-utils/src/user_authorization.rs +++ b/crates/axum-utils/src/user_authorization.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, error::Error}; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 40c46897a..22def30c7 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-cli" version.workspace = true diff --git a/crates/cli/build.rs b/crates/cli/build.rs index 2615b1284..7bad5d337 100644 --- a/crates/cli/build.rs +++ b/crates/cli/build.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use vergen_gitcl::{Emitter, GitclBuilder, RustcBuilder}; diff --git a/crates/cli/src/app_state.rs b/crates/cli/src/app_state.rs index 55b592aea..dd7dd6c99 100644 --- a/crates/cli/src/app_state.rs +++ b/crates/cli/src/app_state.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{convert::Infallible, net::IpAddr, sync::Arc}; diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index 00600026c..73c100e2c 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::process::ExitCode; @@ -72,7 +72,7 @@ impl Options { SC::Dump { output } => { let _span = info_span!("cli.config.dump").entered(); - let config = RootConfig::extract(figment)?; + let config = RootConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; let config = serde_yaml::to_string(&config)?; if let Some(output) = output { @@ -88,7 +88,7 @@ impl Options { SC::Check => { let _span = info_span!("cli.config.check").entered(); - let _config = RootConfig::extract(figment)?; + let _config = RootConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; info!("Configuration file looks good"); } @@ -105,7 +105,8 @@ impl Options { if !synapse_config.is_empty() { info!("Adjusting MAS config to match Synapse config from {synapse_config:?}"); - let synapse_config = syn2mas::synapse_config::Config::load(&synapse_config)?; + let synapse_config = syn2mas::synapse_config::Config::load(&synapse_config) + .map_err(anyhow::Error::from_boxed)?; config = synapse_config.adjust_mas_config(config, &mut rng, clock.now()); } @@ -121,7 +122,7 @@ impl Options { } SC::Sync { prune, dry_run } => { - let config = SyncConfig::extract(figment)?; + let config = SyncConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; let clock = SystemClock::default(); let encrypter = config.secrets.encrypter().await?; diff --git a/crates/cli/src/commands/database.rs b/crates/cli/src/commands/database.rs index 0283e5049..519536fff 100644 --- a/crates/cli/src/commands/database.rs +++ b/crates/cli/src/commands/database.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::process::ExitCode; @@ -30,7 +30,8 @@ enum Subcommand { impl Options { pub async fn run(self, figment: &Figment) -> anyhow::Result { let _span = info_span!("cli.database.migrate").entered(); - let config = DatabaseConfig::extract_or_default(figment)?; + let config = + DatabaseConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&config).await?; // Run pending migrations diff --git a/crates/cli/src/commands/debug.rs b/crates/cli/src/commands/debug.rs index 2c004974f..bb87c5e81 100644 --- a/crates/cli/src/commands/debug.rs +++ b/crates/cli/src/commands/debug.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::process::ExitCode; @@ -41,13 +41,16 @@ impl Options { match self.subcommand { SC::Policy { with_dynamic_data } => { let _span = info_span!("cli.debug.policy").entered(); - let config = PolicyConfig::extract_or_default(figment)?; - let matrix_config = MatrixConfig::extract(figment)?; + let config = + PolicyConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?; + let matrix_config = + MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; info!("Loading and compiling the policy module"); let policy_factory = policy_factory_from_config(&config, &matrix_config).await?; if with_dynamic_data { - let database_config = DatabaseConfig::extract(figment)?; + let database_config = + DatabaseConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; let pool = database_pool_from_config(&database_config).await?; let repository_factory = PgRepositoryFactory::new(pool.clone()); load_policy_factory_dynamic_data(&policy_factory, &repository_factory).await?; diff --git a/crates/cli/src/commands/doctor.rs b/crates/cli/src/commands/doctor.rs index 28ab4e919..d39a5cd52 100644 --- a/crates/cli/src/commands/doctor.rs +++ b/crates/cli/src/commands/doctor.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Diagnostic utility to check the health of the deployment //! @@ -26,14 +26,13 @@ const DOCS_BASE: &str = "https://element-hq.github.io/matrix-authentication-serv pub(super) struct Options {} impl Options { - #[allow(clippy::too_many_lines)] pub async fn run(self, figment: &Figment) -> anyhow::Result { let _span = info_span!("cli.doctor").entered(); info!( "💡 Running diagnostics, make sure that both MAS and Synapse are running, and that MAS is using the same configuration files as this tool." ); - let config = RootConfig::extract(figment)?; + let config = RootConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; // We'll need an HTTP client let http_client = mas_http::reqwest_client(); diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index d2f13f0c5..d56bafa76 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::BTreeMap, process::ExitCode}; @@ -149,6 +149,10 @@ enum Subcommand { UnlockUser { /// User to unlock username: String, + + /// Whether to reactivate the user if it had been deactivated + #[arg(long)] + reactivate: bool, }, /// Register a user @@ -203,7 +207,6 @@ enum Subcommand { } impl Options { - #[allow(clippy::too_many_lines)] pub async fn run(self, figment: &Figment) -> anyhow::Result { use Subcommand as SC; let clock = SystemClock::default(); @@ -219,8 +222,10 @@ impl Options { let _span = info_span!("cli.manage.set_password", user.username = %username).entered(); - let database_config = DatabaseConfig::extract_or_default(figment)?; - let passwords_config = PasswordsConfig::extract_or_default(figment)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let passwords_config = PasswordsConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&database_config).await?; let password_manager = password_manager_from_config(&passwords_config).await?; @@ -260,7 +265,8 @@ impl Options { ) .entered(); - let database_config = DatabaseConfig::extract_or_default(figment)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&database_config).await?; let txn = conn.begin().await?; let mut repo = PgRepository::from_conn(txn); @@ -314,7 +320,12 @@ impl Options { admin, device_id, } => { - let database_config = DatabaseConfig::extract_or_default(figment)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let matrix_config = + MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; + let http_client = mas_http::reqwest_client(); + let homeserver = homeserver_connection_from_config(&matrix_config, http_client); let mut conn = database_connection_from_config(&database_config).await?; let txn = conn.begin().await?; let mut repo = PgRepository::from_conn(txn); @@ -331,6 +342,24 @@ impl Options { Device::generate(&mut rng) }; + if let Err(e) = homeserver + .upsert_device(&user.username, device.as_str(), None) + .await + { + error!( + error = &*e, + "Could not create the device on the homeserver, aborting" + ); + + // Schedule a device sync job to remove the potential leftover device + repo.queue_job() + .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user)) + .await?; + + repo.into_inner().commit().await?; + return Ok(ExitCode::FAILURE); + } + let compat_session = repo .compat_session() .add(&mut rng, &clock, &user, device, None, admin, None) @@ -372,7 +401,8 @@ impl Options { (Some(_), true) => unreachable!(), // This should be handled by the clap group }; - let database_config = DatabaseConfig::extract_or_default(figment)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&database_config).await?; let txn = conn.begin().await?; let mut repo = PgRepository::from_conn(txn); @@ -399,7 +429,8 @@ impl Options { SC::ProvisionAllUsers => { let _span = info_span!("cli.manage.provision_all_users").entered(); - let database_config = DatabaseConfig::extract_or_default(figment)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&database_config).await?; let mut txn = conn.begin().await?; @@ -425,7 +456,8 @@ impl Options { SC::KillSessions { username, dry_run } => { let _span = info_span!("cli.manage.kill_sessions", user.username = username).entered(); - let database_config = DatabaseConfig::extract_or_default(figment)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&database_config).await?; let txn = conn.begin().await?; let mut repo = PgRepository::from_conn(txn); @@ -497,7 +529,8 @@ impl Options { deactivate, } => { let _span = info_span!("cli.manage.lock_user", user.username = username).entered(); - let config = DatabaseConfig::extract_or_default(figment)?; + let config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&config).await?; let txn = conn.begin().await?; let mut repo = PgRepository::from_conn(txn); @@ -527,9 +560,14 @@ impl Options { Ok(ExitCode::SUCCESS) } - SC::UnlockUser { username } => { - let _span = info_span!("cli.manage.lock_user", user.username = username).entered(); - let config = DatabaseConfig::extract_or_default(figment)?; + SC::UnlockUser { + username, + reactivate, + } => { + let _span = + info_span!("cli.manage.unlock_user", user.username = username).entered(); + let config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let mut conn = database_connection_from_config(&config).await?; let txn = conn.begin().await?; let mut repo = PgRepository::from_conn(txn); @@ -540,10 +578,14 @@ impl Options { .await? .context("User not found")?; - warn!(%user.id, "User scheduling user reactivation"); - repo.queue_job() - .schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user)) - .await?; + if reactivate { + warn!(%user.id, "Scheduling user reactivation"); + repo.queue_job() + .schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user)) + .await?; + } else { + repo.user().unlock(user).await?; + } repo.into_inner().commit().await?; @@ -562,9 +604,12 @@ impl Options { ignore_password_complexity, } => { let http_client = mas_http::reqwest_client(); - let password_config = PasswordsConfig::extract_or_default(figment)?; - let database_config = DatabaseConfig::extract_or_default(figment)?; - let matrix_config = MatrixConfig::extract(figment)?; + let password_config = PasswordsConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let database_config = DatabaseConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let matrix_config = + MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; let password_manager = password_manager_from_config(&password_config).await?; let homeserver = homeserver_connection_from_config(&matrix_config, http_client); diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 9f0938083..e8cef3b1f 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::process::ExitCode; diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index dcdbca0d3..891400176 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::BTreeSet, process::ExitCode, sync::Arc, time::Duration}; @@ -55,11 +55,10 @@ pub(super) struct Options { } impl Options { - #[allow(clippy::too_many_lines)] pub async fn run(self, figment: &Figment) -> anyhow::Result { let span = info_span!("cli.run.init").entered(); let mut shutdown = LifecycleManager::new()?; - let config = AppConfig::extract(figment)?; + let config = AppConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; info!(version = crate::VERSION, "Starting up"); @@ -101,8 +100,10 @@ impl Options { } else { // Sync the configuration with the database let mut conn = pool.acquire().await?; - let clients_config = ClientsConfig::extract_or_default(figment)?; - let upstream_oauth2_config = UpstreamOAuth2Config::extract_or_default(figment)?; + let clients_config = + ClientsConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?; + let upstream_oauth2_config = UpstreamOAuth2Config::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; crate::sync::config_sync( upstream_oauth2_config, @@ -173,8 +174,9 @@ impl Options { test_mailer_in_background(&mailer, Duration::from_secs(30)); info!("Starting task worker"); - mas_tasks::init( + mas_tasks::init_and_run( PgRepositoryFactory::new(pool.clone()), + SystemClock::default(), &mailer, homeserver_connection.clone(), url_builder.clone(), diff --git a/crates/cli/src/commands/syn2mas.rs b/crates/cli/src/commands/syn2mas.rs index 22194953e..50dd99778 100644 --- a/crates/cli/src/commands/syn2mas.rs +++ b/crates/cli/src/commands/syn2mas.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, process::ExitCode, time::Duration}; @@ -88,7 +88,6 @@ const NUM_WRITER_CONNECTIONS: usize = 8; impl Options { #[tracing::instrument("cli.syn2mas.run", skip_all)] - #[allow(clippy::too_many_lines)] pub async fn run(self, figment: &Figment) -> anyhow::Result { if self.synapse_configuration_files.is_empty() { error!("Please specify the path to the Synapse configuration file(s)."); @@ -96,6 +95,7 @@ impl Options { } let synapse_config = synapse_config::Config::load(&self.synapse_configuration_files) + .map_err(anyhow::Error::from_boxed) .context("Failed to load Synapse configuration")?; // Establish a connection to Synapse's Postgres database @@ -111,7 +111,8 @@ impl Options { .await .context("could not connect to Synapse Postgres database")?; - let config = DatabaseConfig::extract_or_default(figment)?; + let config = + DatabaseConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?; let mut mas_connection = database_connection_from_config_with_options( &config, @@ -131,7 +132,7 @@ impl Options { // First perform a config sync // This is crucial to ensure we register upstream OAuth providers // in the MAS database - let config = SyncConfig::extract(figment)?; + let config = SyncConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; let clock = SystemClock::default(); let encrypter = config.secrets.encrypter().await?; @@ -213,7 +214,8 @@ impl Options { Subcommand::Migrate { dry_run } => { let provider_id_mappings: HashMap = { - let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(figment)?; + let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; mas_oauth2 .providers @@ -252,7 +254,8 @@ impl Options { let occasional_progress_logger_task = tokio::spawn(occasional_progress_logger(progress.clone())); - let mas_matrix = MatrixConfig::extract(figment)?; + let mas_matrix = + MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; syn2mas::migrate( reader, writer, diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index 153565df7..011c5d268 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::process::ExitCode; @@ -37,13 +37,20 @@ impl Options { SC::Check => { let _span = info_span!("cli.templates.check").entered(); - let template_config = TemplatesConfig::extract_or_default(figment)?; - let branding_config = BrandingConfig::extract_or_default(figment)?; - let matrix_config = MatrixConfig::extract(figment)?; - let experimental_config = ExperimentalConfig::extract_or_default(figment)?; - let password_config = PasswordsConfig::extract_or_default(figment)?; - let account_config = AccountConfig::extract_or_default(figment)?; - let captcha_config = CaptchaConfig::extract_or_default(figment)?; + let template_config = TemplatesConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let branding_config = BrandingConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let matrix_config = + MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; + let experimental_config = ExperimentalConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let password_config = PasswordsConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let account_config = AccountConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; + let captcha_config = CaptchaConfig::extract_or_default(figment) + .map_err(anyhow::Error::from_boxed)?; let clock = SystemClock::default(); // XXX: we should disallow SeedableRng::from_entropy diff --git a/crates/cli/src/commands/worker.rs b/crates/cli/src/commands/worker.rs index f13a1ae3c..bd15e3b1f 100644 --- a/crates/cli/src/commands/worker.rs +++ b/crates/cli/src/commands/worker.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{process::ExitCode, time::Duration}; @@ -10,6 +10,7 @@ use clap::Parser; use figment::Figment; use mas_config::{AppConfig, ConfigurationSection}; use mas_router::UrlBuilder; +use mas_storage::SystemClock; use mas_storage_pg::PgRepositoryFactory; use tracing::{info, info_span}; @@ -28,7 +29,7 @@ impl Options { pub async fn run(self, figment: &Figment) -> anyhow::Result { let shutdown = LifecycleManager::new()?; let span = info_span!("cli.worker.init").entered(); - let config = AppConfig::extract(figment)?; + let config = AppConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; // Connect to the database info!("Connecting to the database"); @@ -63,8 +64,9 @@ impl Options { drop(config); info!("Starting task scheduler"); - mas_tasks::init( + mas_tasks::init_and_run( PgRepositoryFactory::new(pool.clone()), + SystemClock::default(), &mailer, conn, url_builder, diff --git a/crates/cli/src/lifecycle.rs b/crates/cli/src/lifecycle.rs index 4a2c429d1..e44162936 100644 --- a/crates/cli/src/lifecycle.rs +++ b/crates/cli/src/lifecycle.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{process::ExitCode, time::Duration}; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index f0da47c09..5df40da83 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::module_name_repetitions)] @@ -115,8 +115,9 @@ async fn try_main() -> anyhow::Result { // Load the base configuration files let figment = opts.figment(); - let telemetry_config = - TelemetryConfig::extract_or_default(&figment).context("Failed to load telemetry config")?; + let telemetry_config = TelemetryConfig::extract_or_default(&figment) + .map_err(anyhow::Error::from_boxed) + .context("Failed to load telemetry config")?; // Setup Sentry let sentry = sentry::init(( @@ -127,8 +128,6 @@ async fn try_main() -> anyhow::Result { release: Some(VERSION.into()), sample_rate: telemetry_config.sentry.sample_rate.unwrap_or(1.0), traces_sample_rate: telemetry_config.sentry.traces_sample_rate.unwrap_or(0.0), - auto_session_tracking: true, - session_mode: sentry::SessionMode::Request, ..Default::default() }, )); diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index eefd7211c..9ce9b3a52 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs}, @@ -205,7 +205,6 @@ async fn log_response_middleware( response } -#[allow(clippy::too_many_lines)] pub fn build_router( state: AppState, resources: &[HttpResource], @@ -269,7 +268,7 @@ pub fn build_router( } mas_config::HttpResource::OAuth => router.merge(mas_handlers::api_router::()), mas_config::HttpResource::Compat => { - router.merge(mas_handlers::compat_router::()) + router.merge(mas_handlers::compat_router::(templates.clone())) } mas_config::HttpResource::AdminApi => { let (_, api_router) = mas_handlers::admin_api_router::(); @@ -333,7 +332,7 @@ pub fn build_router( // which is the other way around compared to `tower::ServiceBuilder`. // So even if the Sentry docs has an example that does // 'NewSentryHttpLayer then SentryHttpLayer', we must do the opposite. - .layer(SentryHttpLayer::with_transaction()) + .layer(SentryHttpLayer::new().enable_transaction()) .layer(NewSentryLayer::new_from_top()) .with_state(state) } diff --git a/crates/cli/src/sync.rs b/crates/cli/src/sync.rs index 0c1063607..ff33106af 100644 --- a/crates/cli/src/sync.rs +++ b/crates/cli/src/sync.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Utilities to synchronize the configuration file with the database. @@ -37,6 +37,19 @@ fn map_import_action( } } +fn map_import_on_conflict( + config: mas_config::UpstreamOAuth2OnConflict, +) -> mas_data_model::UpstreamOAuthProviderOnConflict { + match config { + mas_config::UpstreamOAuth2OnConflict::Add => { + mas_data_model::UpstreamOAuthProviderOnConflict::Add + } + mas_config::UpstreamOAuth2OnConflict::Fail => { + mas_data_model::UpstreamOAuthProviderOnConflict::Fail + } + } +} + fn map_claims_imports( config: &mas_config::UpstreamOAuth2ClaimsImports, ) -> mas_data_model::UpstreamOAuthProviderClaimsImports { @@ -44,9 +57,10 @@ fn map_claims_imports( subject: mas_data_model::UpstreamOAuthProviderSubjectPreference { template: config.subject.template.clone(), }, - localpart: mas_data_model::UpstreamOAuthProviderImportPreference { + localpart: mas_data_model::UpstreamOAuthProviderLocalpartPreference { action: map_import_action(config.localpart.action), template: config.localpart.template.clone(), + on_conflict: map_import_on_conflict(config.localpart.on_conflict), }, displayname: mas_data_model::UpstreamOAuthProviderImportPreference { action: map_import_action(config.displayname.action), @@ -276,6 +290,18 @@ pub async fn config_sync( } }; + let on_backchannel_logout = match provider.on_backchannel_logout { + mas_config::UpstreamOAuth2OnBackchannelLogout::DoNothing => { + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing + } + mas_config::UpstreamOAuth2OnBackchannelLogout::LogoutBrowserOnly => { + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::LogoutBrowserOnly + } + mas_config::UpstreamOAuth2OnBackchannelLogout::LogoutAll => { + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::LogoutAll + } + }; + repo.upstream_oauth_provider() .upsert( clock, @@ -306,6 +332,7 @@ pub async fn config_sync( .collect(), forward_login_hint: provider.forward_login_hint, ui_order, + on_backchannel_logout, }, ) .await?; diff --git a/crates/cli/src/telemetry.rs b/crates/cli/src/telemetry.rs index a09207e44..630dcbfca 100644 --- a/crates/cli/src/telemetry.rs +++ b/crates/cli/src/telemetry.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod tokio; diff --git a/crates/cli/src/telemetry/tokio.rs b/crates/cli/src/telemetry/tokio.rs index 49c7ac0ef..7346a7620 100644 --- a/crates/cli/src/telemetry/tokio.rs +++ b/crates/cli/src/telemetry/tokio.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use opentelemetry::KeyValue; use tokio::runtime::RuntimeMetrics; @@ -9,7 +9,6 @@ use tokio::runtime::RuntimeMetrics; use super::METER; /// Install metrics for the tokio runtime. -#[allow(clippy::too_many_lines)] pub fn observe(metrics: RuntimeMetrics) { { let metrics = metrics.clone(); diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index b6725a0eb..c181cd15c 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{sync::Arc, time::Duration}; @@ -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/Cargo.toml b/crates/config/Cargo.toml index 35b7f83d1..c6eb76d7c 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-config" version.workspace = true diff --git a/crates/config/src/bin/schema.rs b/crates/config/src/bin/schema.rs index 3f409d406..83a28c8c9 100644 --- a/crates/config/src/bin/schema.rs +++ b/crates/config/src/bin/schema.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use schemars::r#gen::SchemaSettings; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index d8230a9fb..cdf68e420 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(missing_docs, rustdoc::missing_crate_level_docs)] #![allow(clippy::module_name_repetitions)] diff --git a/crates/config/src/schema.rs b/crates/config/src/schema.rs index 2a9aaa914..a3c732419 100644 --- a/crates/config/src/schema.rs +++ b/crates/config/src/schema.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Useful JSON Schema definitions diff --git a/crates/config/src/sections/account.rs b/crates/config/src/sections/account.rs index a9d51afbb..47efa0162 100644 --- a/crates/config/src/sections/account.rs +++ b/crates/config/src/sections/account.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/config/src/sections/branding.rs b/crates/config/src/sections/branding.rs index 91bba2cf9..ec36f1612 100644 --- a/crates/config/src/sections/branding.rs +++ b/crates/config/src/sections/branding.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/config/src/sections/captcha.rs b/crates/config/src/sections/captcha.rs index 58b30799d..962d1f342 100644 --- a/crates/config/src/sections/captcha.rs +++ b/crates/config/src/sections/captcha.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::Error}; @@ -51,7 +51,10 @@ impl CaptchaConfig { impl ConfigurationSection for CaptchaConfig { const PATH: Option<&'static str> = Some("captcha"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { let metadata = figment.find_metadata(Self::PATH.unwrap()); let error_on_field = |mut error: figment::error::Error, field: &'static str| { @@ -67,11 +70,11 @@ impl ConfigurationSection for CaptchaConfig { if let Some(CaptchaServiceKind::RecaptchaV2) = self.service { if self.site_key.is_none() { - return Err(missing_field("site_key")); + return Err(missing_field("site_key").into()); } if self.secret_key.is_none() { - return Err(missing_field("secret_key")); + return Err(missing_field("secret_key").into()); } } diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 2a0469677..5d2b0d453 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -1,12 +1,11 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::ops::Deref; -use figment::Figment; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_jose::jwk::PublicJsonWebKeySet; use schemars::JsonSchema; @@ -104,7 +103,7 @@ pub struct ClientConfig { } impl ClientConfig { - fn validate(&self) -> Result<(), figment::error::Error> { + fn validate(&self) -> Result<(), Box> { let auth_method = self.client_auth_method; match self.client_auth_method { ClientAuthMethodConfig::PrivateKeyJwt => { @@ -112,20 +111,20 @@ impl ClientConfig { let error = figment::error::Error::custom( "jwks or jwks_uri is required for private_key_jwt", ); - return Err(error.with_path("client_auth_method")); + return Err(Box::new(error.with_path("client_auth_method"))); } if self.jwks.is_some() && self.jwks_uri.is_some() { let error = figment::error::Error::custom("jwks and jwks_uri are mutually exclusive"); - return Err(error.with_path("jwks")); + return Err(Box::new(error.with_path("jwks"))); } if self.client_secret.is_some() { let error = figment::error::Error::custom( "client_secret is not allowed with private_key_jwt", ); - return Err(error.with_path("client_secret")); + return Err(Box::new(error.with_path("client_secret"))); } } @@ -136,21 +135,21 @@ impl ClientConfig { let error = figment::error::Error::custom(format!( "client_secret is required for {auth_method}" )); - return Err(error.with_path("client_auth_method")); + return Err(Box::new(error.with_path("client_auth_method"))); } if self.jwks.is_some() { let error = figment::error::Error::custom(format!( "jwks is not allowed with {auth_method}" )); - return Err(error.with_path("jwks")); + return Err(Box::new(error.with_path("jwks"))); } if self.jwks_uri.is_some() { let error = figment::error::Error::custom(format!( "jwks_uri is not allowed with {auth_method}" )); - return Err(error.with_path("jwks_uri")); + return Err(Box::new(error.with_path("jwks_uri"))); } } @@ -159,21 +158,21 @@ impl ClientConfig { let error = figment::error::Error::custom( "client_secret is not allowed with none authentication method", ); - return Err(error.with_path("client_secret")); + return Err(Box::new(error.with_path("client_secret"))); } if self.jwks.is_some() { let error = figment::error::Error::custom( "jwks is not allowed with none authentication method", ); - return Err(error); + return Err(Box::new(error)); } if self.jwks_uri.is_some() { let error = figment::error::Error::custom( "jwks_uri is not allowed with none authentication method", ); - return Err(error); + return Err(Box::new(error)); } } } @@ -232,7 +231,10 @@ impl IntoIterator for ClientsConfig { impl ConfigurationSection for ClientsConfig { const PATH: Option<&'static str> = Some("clients"); - fn validate(&self, figment: &Figment) -> Result<(), figment::error::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { for (index, client) in self.0.iter().enumerate() { client.validate().map_err(|mut err| { // Save the error location information in the error diff --git a/crates/config/src/sections/database.rs b/crates/config/src/sections/database.rs index e2b701c60..4830a4016 100644 --- a/crates/config/src/sections/database.rs +++ b/crates/config/src/sections/database.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{num::NonZeroU32, time::Duration}; @@ -222,13 +222,16 @@ pub struct DatabaseConfig { impl ConfigurationSection for DatabaseConfig { const PATH: Option<&'static str> = Some("database"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { let metadata = figment.find_metadata(Self::PATH.unwrap()); let annotate = |mut error: figment::Error| { error.metadata = metadata.cloned(); error.profile = Some(figment::Profile::Default); error.path = vec![Self::PATH.unwrap().to_owned()]; - Err(error) + error }; // Check that the user did not specify both `uri` and the split options at the @@ -241,37 +244,41 @@ impl ConfigurationSection for DatabaseConfig { || self.database.is_some(); if self.uri.is_some() && has_split_options { - return annotate(figment::error::Error::from( + return Err(annotate(figment::error::Error::from( "uri must not be specified if host, port, socket, username, password, or database are specified".to_owned(), - )); + )).into()); } if self.ssl_ca.is_some() && self.ssl_ca_file.is_some() { - return annotate(figment::error::Error::from( + return Err(annotate(figment::error::Error::from( "ssl_ca must not be specified if ssl_ca_file is specified".to_owned(), - )); + )) + .into()); } if self.ssl_certificate.is_some() && self.ssl_certificate_file.is_some() { - return annotate(figment::error::Error::from( + return Err(annotate(figment::error::Error::from( "ssl_certificate must not be specified if ssl_certificate_file is specified" .to_owned(), - )); + )) + .into()); } if self.ssl_key.is_some() && self.ssl_key_file.is_some() { - return annotate(figment::error::Error::from( + return Err(annotate(figment::error::Error::from( "ssl_key must not be specified if ssl_key_file is specified".to_owned(), - )); + )) + .into()); } if (self.ssl_key.is_some() || self.ssl_key_file.is_some()) ^ (self.ssl_certificate.is_some() || self.ssl_certificate_file.is_some()) { - return annotate(figment::error::Error::from( + return Err(annotate(figment::error::Error::from( "both a ssl_certificate and a ssl_key must be set at the same time or none of them" .to_owned(), - )); + )) + .into()); } Ok(()) diff --git a/crates/config/src/sections/email.rs b/crates/config/src/sections/email.rs index 18f86df13..3df0c99db 100644 --- a/crates/config/src/sections/email.rs +++ b/crates/config/src/sections/email.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(deprecated)] @@ -175,7 +175,10 @@ impl Default for EmailConfig { impl ConfigurationSection for EmailConfig { const PATH: Option<&'static str> = Some("email"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { let metadata = figment.find_metadata(Self::PATH.unwrap()); let error_on_field = |mut error: figment::error::Error, field: &'static str| { @@ -201,29 +204,29 @@ impl ConfigurationSection for EmailConfig { EmailTransportKind::Smtp => { if let Err(e) = Mailbox::from_str(&self.from) { - return Err(error_on_field(figment::error::Error::custom(e), "from")); + return Err(error_on_field(figment::error::Error::custom(e), "from").into()); } if let Err(e) = Mailbox::from_str(&self.reply_to) { - return Err(error_on_field(figment::error::Error::custom(e), "reply_to")); + return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into()); } match (self.username.is_some(), self.password.is_some()) { (true, true) | (false, false) => {} (true, false) => { - return Err(missing_field("password")); + return Err(missing_field("password").into()); } (false, true) => { - return Err(missing_field("username")); + return Err(missing_field("username").into()); } } if self.mode.is_none() { - return Err(missing_field("mode")); + return Err(missing_field("mode").into()); } if self.hostname.is_none() { - return Err(missing_field("hostname")); + return Err(missing_field("hostname").into()); } if self.command.is_some() { @@ -239,7 +242,8 @@ impl ConfigurationSection for EmailConfig { "username", "password", ], - )); + ) + .into()); } } @@ -247,35 +251,35 @@ impl ConfigurationSection for EmailConfig { let expected_fields = &["from", "reply_to", "transport", "command"]; if let Err(e) = Mailbox::from_str(&self.from) { - return Err(error_on_field(figment::error::Error::custom(e), "from")); + return Err(error_on_field(figment::error::Error::custom(e), "from").into()); } if let Err(e) = Mailbox::from_str(&self.reply_to) { - return Err(error_on_field(figment::error::Error::custom(e), "reply_to")); + return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into()); } if self.command.is_none() { - return Err(missing_field("command")); + return Err(missing_field("command").into()); } if self.mode.is_some() { - return Err(unexpected_field("mode", expected_fields)); + return Err(unexpected_field("mode", expected_fields).into()); } if self.hostname.is_some() { - return Err(unexpected_field("hostname", expected_fields)); + return Err(unexpected_field("hostname", expected_fields).into()); } if self.port.is_some() { - return Err(unexpected_field("port", expected_fields)); + return Err(unexpected_field("port", expected_fields).into()); } if self.username.is_some() { - return Err(unexpected_field("username", expected_fields)); + return Err(unexpected_field("username", expected_fields).into()); } if self.password.is_some() { - return Err(unexpected_field("password", expected_fields)); + return Err(unexpected_field("password", expected_fields).into()); } } } diff --git a/crates/config/src/sections/experimental.rs b/crates/config/src/sections/experimental.rs index 35e29e0de..c6c50e88d 100644 --- a/crates/config/src/sections/experimental.rs +++ b/crates/config/src/sections/experimental.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::Duration; use schemars::JsonSchema; diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index 8bf4a7588..c75d47fd3 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(deprecated)] @@ -412,7 +412,10 @@ impl Default for HttpConfig { impl ConfigurationSection for HttpConfig { const PATH: Option<&'static str> = Some("http"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { for (index, listener) in self.listeners.iter().enumerate() { let annotate = |mut error: figment::Error| { error.metadata = figment @@ -424,49 +427,57 @@ impl ConfigurationSection for HttpConfig { "listeners".to_owned(), index.to_string(), ]; - Err(error) + error }; if listener.resources.is_empty() { - return annotate(figment::Error::from("listener has no resources".to_owned())); + return Err( + annotate(figment::Error::from("listener has no resources".to_owned())).into(), + ); } if listener.binds.is_empty() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "listener does not bind to any address".to_owned(), - )); + )) + .into()); } if let Some(tls_config) = &listener.tls { if tls_config.certificate.is_some() && tls_config.certificate_file.is_some() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "Only one of `certificate` or `certificate_file` can be set at a time" .to_owned(), - )); + )) + .into()); } if tls_config.certificate.is_none() && tls_config.certificate_file.is_none() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "TLS configuration is missing a certificate".to_owned(), - )); + )) + .into()); } if tls_config.key.is_some() && tls_config.key_file.is_some() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "Only one of `key` or `key_file` can be set at a time".to_owned(), - )); + )) + .into()); } if tls_config.key.is_none() && tls_config.key_file.is_none() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "TLS configuration is missing a private key".to_owned(), - )); + )) + .into()); } if tls_config.password.is_some() && tls_config.password_file.is_some() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "Only one of `password` or `password_file` can be set at a time".to_owned(), - )); + )) + .into()); } } } diff --git a/crates/config/src/sections/matrix.rs b/crates/config/src/sections/matrix.rs index d5e35907e..e035b7d79 100644 --- a/crates/config/src/sections/matrix.rs +++ b/crates/config/src/sections/matrix.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use rand::{ Rng, @@ -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/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 9a9fc9de8..f992d8698 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use rand::Rng; use schemars::JsonSchema; @@ -52,7 +52,9 @@ pub use self::{ upstream_oauth2::{ ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode, EmailImportPreference as UpstreamOAuth2EmailImportPreference, - ImportAction as UpstreamOAuth2ImportAction, PkceMethod as UpstreamOAuth2PkceMethod, + ImportAction as UpstreamOAuth2ImportAction, + OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout, + OnConflict as UpstreamOAuth2OnConflict, PkceMethod as UpstreamOAuth2PkceMethod, Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode, TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config, }, @@ -128,7 +130,10 @@ pub struct RootConfig { } impl ConfigurationSection for RootConfig { - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { self.clients.validate(figment)?; self.http.validate(figment)?; self.database.validate(figment)?; @@ -247,7 +252,10 @@ pub struct AppConfig { } impl ConfigurationSection for AppConfig { - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { self.http.validate(figment)?; self.database.validate(figment)?; self.templates.validate(figment)?; @@ -283,7 +291,10 @@ pub struct SyncConfig { } impl ConfigurationSection for SyncConfig { - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { self.database.validate(figment)?; self.secrets.validate(figment)?; self.clients.validate(figment)?; diff --git a/crates/config/src/sections/passwords.rs b/crates/config/src/sections/passwords.rs index a72f2efd7..b15c009fc 100644 --- a/crates/config/src/sections/passwords.rs +++ b/crates/config/src/sections/passwords.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::cmp::Reverse; @@ -72,12 +72,15 @@ impl Default for PasswordsConfig { impl ConfigurationSection for PasswordsConfig { const PATH: Option<&'static str> = Some("passwords"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { let annotate = |mut error: figment::Error| { error.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned(); error.profile = Some(figment::Profile::Default); error.path = vec![Self::PATH.unwrap().to_owned()]; - Err(error) + error }; if !self.enabled { @@ -86,16 +89,18 @@ impl ConfigurationSection for PasswordsConfig { } if self.schemes.is_empty() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "Requires at least one password scheme in the config".to_owned(), - )); + )) + .into()); } for scheme in &self.schemes { if scheme.secret.is_some() && scheme.secret_file.is_some() { - return annotate(figment::Error::from( + return Err(annotate(figment::Error::from( "Cannot specify both `secret` and `secret_file`".to_owned(), - )); + )) + .into()); } } diff --git a/crates/config/src/sections/policy.rs b/crates/config/src/sections/policy.rs index 03ec09e44..37d052ade 100644 --- a/crates/config/src/sections/policy.rs +++ b/crates/config/src/sections/policy.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use camino::Utf8PathBuf; use schemars::JsonSchema; diff --git a/crates/config/src/sections/rate_limiting.rs b/crates/config/src/sections/rate_limiting.rs index d95a2ab28..0b7c95dbc 100644 --- a/crates/config/src/sections/rate_limiting.rs +++ b/crates/config/src/sections/rate_limiting.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{num::NonZeroU32, time::Duration}; @@ -117,7 +117,10 @@ pub struct RateLimiterConfiguration { impl ConfigurationSection for RateLimitingConfig { const PATH: Option<&'static str> = Some("rate_limiting"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { let metadata = figment.find_metadata(Self::PATH.unwrap()); let error_on_field = |mut error: figment::error::Error, field: &'static str| { @@ -154,25 +157,21 @@ impl ConfigurationSection for RateLimitingConfig { }; if let Some(error) = error_on_limiter(&self.account_recovery.per_ip) { - return Err(error_on_nested_field(error, "account_recovery", "per_ip")); + return Err(error_on_nested_field(error, "account_recovery", "per_ip").into()); } if let Some(error) = error_on_limiter(&self.account_recovery.per_address) { - return Err(error_on_nested_field( - error, - "account_recovery", - "per_address", - )); + return Err(error_on_nested_field(error, "account_recovery", "per_address").into()); } if let Some(error) = error_on_limiter(&self.registration) { - return Err(error_on_field(error, "registration")); + return Err(error_on_field(error, "registration").into()); } if let Some(error) = error_on_limiter(&self.login.per_ip) { - return Err(error_on_nested_field(error, "login", "per_ip")); + return Err(error_on_nested_field(error, "login", "per_ip").into()); } if let Some(error) = error_on_limiter(&self.login.per_account) { - return Err(error_on_nested_field(error, "login", "per_account")); + return Err(error_on_nested_field(error, "login", "per_account").into()); } Ok(()) diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index e93817b68..7886e9a57 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::borrow::Cow; @@ -149,10 +149,10 @@ impl KeyConfig { /// Returns the password in case any is provided. /// /// If `password_file` was given, the password is read from that file. - async fn password(&self) -> anyhow::Result>> { + async fn password(&self) -> anyhow::Result>> { Ok(match &self.password { - Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read_to_string(path).await?)), - Some(Password::Value(password)) => Some(Cow::Borrowed(password)), + Some(Password::File(path)) => Some(Cow::Owned(tokio::fs::read(path).await?)), + Some(Password::Value(password)) => Some(Cow::Borrowed(password.as_bytes())), None => None, }) } @@ -160,10 +160,10 @@ impl KeyConfig { /// Returns the key. /// /// If `key_file` was given, the key is read from that file. - async fn key(&self) -> anyhow::Result> { + async fn key(&self) -> anyhow::Result> { Ok(match &self.key { - Key::File(path) => Cow::Owned(tokio::fs::read_to_string(path).await?), - Key::Value(key) => Cow::Borrowed(key), + Key::File(path) => Cow::Owned(tokio::fs::read(path).await?), + Key::Value(key) => Cow::Borrowed(key.as_bytes()), }) } @@ -174,8 +174,8 @@ impl KeyConfig { let (key, password) = try_join(self.key(), self.password()).await?; let private_key = match password { - Some(password) => PrivateKey::load_encrypted(key.as_bytes(), password.as_bytes())?, - None => PrivateKey::load(key.as_bytes())?, + Some(password) => PrivateKey::load_encrypted(&key, password)?, + None => PrivateKey::load(&key)?, }; Ok(JsonWebKey::new(private_key) @@ -303,6 +303,7 @@ impl ConfigurationSection for SecretsConfig { } impl SecretsConfig { + #[expect(clippy::similar_names, reason = "Key type names are very similar")] #[tracing::instrument(skip_all)] pub(crate) async fn generate(mut rng: R) -> anyhow::Result where @@ -347,7 +348,7 @@ impl SecretsConfig { let ec_p384_key = task::spawn_blocking(move || { let _entered = span.enter(); let ret = PrivateKey::generate_ec_p384(key_rng); - info!("Done generating EC P-256 key"); + info!("Done generating EC P-384 key"); ret }) .await diff --git a/crates/config/src/sections/telemetry.rs b/crates/config/src/sections/telemetry.rs index 0c11e0285..8e2d995e9 100644 --- a/crates/config/src/sections/telemetry.rs +++ b/crates/config/src/sections/telemetry.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::Error as _}; @@ -194,13 +194,17 @@ impl TelemetryConfig { impl ConfigurationSection for TelemetryConfig { const PATH: Option<&'static str> = Some("telemetry"); - fn validate(&self, _figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + _figment: &figment::Figment, + ) -> Result<(), Box> { if let Some(sample_rate) = self.sentry.sample_rate { if !(0.0..=1.0).contains(&sample_rate) { return Err(figment::error::Error::custom( "Sentry sample rate must be between 0.0 and 1.0", ) - .with_path("sentry.sample_rate")); + .with_path("sentry.sample_rate") + .into()); } } @@ -209,7 +213,8 @@ impl ConfigurationSection for TelemetryConfig { return Err(figment::error::Error::custom( "Sentry sample rate must be between 0.0 and 1.0", ) - .with_path("sentry.traces_sample_rate")); + .with_path("sentry.traces_sample_rate") + .into()); } } @@ -218,7 +223,8 @@ impl ConfigurationSection for TelemetryConfig { return Err(figment::error::Error::custom( "Tracing sample rate must be between 0.0 and 1.0", ) - .with_path("tracing.sample_rate")); + .with_path("tracing.sample_rate") + .into()); } } diff --git a/crates/config/src/sections/templates.rs b/crates/config/src/sections/templates.rs index dca53f0e7..5656de44b 100644 --- a/crates/config/src/sections/templates.rs +++ b/crates/config/src/sections/templates.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use camino::Utf8PathBuf; use schemars::JsonSchema; diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index aa6a27254..9b2768423 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::BTreeMap; @@ -33,7 +33,10 @@ impl UpstreamOAuth2Config { impl ConfigurationSection for UpstreamOAuth2Config { const PATH: Option<&'static str> = Some("upstream_oauth2"); - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + fn validate( + &self, + figment: &figment::Figment, + ) -> Result<(), Box> { for (index, provider) in self.providers.iter().enumerate() { let annotate = |mut error: figment::Error| { error.metadata = figment @@ -45,15 +48,16 @@ impl ConfigurationSection for UpstreamOAuth2Config { "providers".to_owned(), index.to_string(), ]; - Err(error) + error }; if !matches!(provider.discovery_mode, DiscoveryMode::Disabled) && provider.issuer.is_none() { - return annotate(figment::Error::custom( + return Err(annotate(figment::Error::custom( "The `issuer` field is required when discovery is enabled", - )); + )) + .into()); } match provider.token_endpoint_auth_method { @@ -61,16 +65,16 @@ impl ConfigurationSection for UpstreamOAuth2Config { | TokenAuthMethod::PrivateKeyJwt | TokenAuthMethod::SignInWithApple => { if provider.client_secret.is_some() { - return annotate(figment::Error::custom( + return Err(annotate(figment::Error::custom( "Unexpected field `client_secret` for the selected authentication method", - )); + )).into()); } } TokenAuthMethod::ClientSecretBasic | TokenAuthMethod::ClientSecretPost | TokenAuthMethod::ClientSecretJwt => { if provider.client_secret.is_none() { - return annotate(figment::Error::missing_field("client_secret")); + return Err(annotate(figment::Error::missing_field("client_secret")).into()); } } } @@ -81,16 +85,17 @@ impl ConfigurationSection for UpstreamOAuth2Config { | TokenAuthMethod::ClientSecretPost | TokenAuthMethod::SignInWithApple => { if provider.token_endpoint_auth_signing_alg.is_some() { - return annotate(figment::Error::custom( + return Err(annotate(figment::Error::custom( "Unexpected field `token_endpoint_auth_signing_alg` for the selected authentication method", - )); + )).into()); } } TokenAuthMethod::ClientSecretJwt | TokenAuthMethod::PrivateKeyJwt => { if provider.token_endpoint_auth_signing_alg.is_none() { - return annotate(figment::Error::missing_field( + return Err(annotate(figment::Error::missing_field( "token_endpoint_auth_signing_alg", - )); + )) + .into()); } } } @@ -98,18 +103,32 @@ impl ConfigurationSection for UpstreamOAuth2Config { match provider.token_endpoint_auth_method { TokenAuthMethod::SignInWithApple => { if provider.sign_in_with_apple.is_none() { - return annotate(figment::Error::missing_field("sign_in_with_apple")); + return Err( + annotate(figment::Error::missing_field("sign_in_with_apple")).into(), + ); } } _ => { if provider.sign_in_with_apple.is_some() { - return annotate(figment::Error::custom( + return Err(annotate(figment::Error::custom( "Unexpected field `sign_in_with_apple` for the selected authentication method", - )); + )).into()); } } } + + if matches!( + provider.claims_imports.localpart.on_conflict, + OnConflict::Add + ) && !matches!( + provider.claims_imports.localpart.action, + ImportAction::Force | ImportAction::Require + ) { + return Err(annotate(figment::Error::custom( + "The field `action` must be either `force` or `require` when `on_conflict` is set to `add`", + )).into()); + } } Ok(()) @@ -183,6 +202,26 @@ impl ImportAction { } } +/// How to handle an existing localpart claim +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum OnConflict { + /// Fails the sso login on conflict + #[default] + Fail, + + /// Adds the oauth identity link, regardless of whether there is an existing + /// link or not + Add, +} + +impl OnConflict { + #[allow(clippy::trivially_copy_pass_by_ref)] + const fn is_default(&self) -> bool { + matches!(self, OnConflict::Fail) + } +} + /// What should be done for the subject attribute #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)] pub struct SubjectImportPreference { @@ -211,6 +250,10 @@ pub struct LocalpartImportPreference { /// If not provided, the default template is `{{ user.preferred_username }}` #[serde(default, skip_serializing_if = "Option::is_none")] pub template: Option, + + /// How to handle conflicts on the claim, default value is `Fail` + #[serde(default, skip_serializing_if = "OnConflict::is_default")] + pub on_conflict: OnConflict, } impl LocalpartImportPreference { @@ -408,6 +451,29 @@ fn is_default_scope(scope: &str) -> bool { scope == default_scope() } +/// What to do when receiving an OIDC Backchannel logout request. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum OnBackchannelLogout { + /// Do nothing + #[default] + DoNothing, + + /// Only log out the MAS 'browser session' started by this OIDC session + LogoutBrowserOnly, + + /// Log out all sessions started by this OIDC session, including MAS + /// 'browser sessions' and client sessions + LogoutAll, +} + +impl OnBackchannelLogout { + #[allow(clippy::trivially_copy_pass_by_ref)] + const fn is_default(&self) -> bool { + matches!(self, OnBackchannelLogout::DoNothing) + } +} + /// Configuration for one upstream OAuth 2 provider. #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -583,4 +649,10 @@ pub struct Provider { /// Defaults to `false`. #[serde(default)] pub forward_login_hint: bool, + + /// What to do when receiving an OIDC Backchannel logout request. + /// + /// Defaults to "do_nothing". + #[serde(default, skip_serializing_if = "OnBackchannelLogout::is_default")] + pub on_backchannel_logout: OnBackchannelLogout, } diff --git a/crates/config/src/util.rs b/crates/config/src/util.rs index c2ffd037b..d6cf58c3f 100644 --- a/crates/config/src/util.rs +++ b/crates/config/src/util.rs @@ -1,10 +1,10 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. -use figment::{Figment, error::Error as FigmentError}; +use figment::Figment; use serde::de::DeserializeOwned; /// Trait implemented by all configuration section to help loading specific part @@ -18,7 +18,10 @@ pub trait ConfigurationSection: Sized + DeserializeOwned { /// # Errors /// /// Returns an error if the configuration is invalid - fn validate(&self, _figment: &Figment) -> Result<(), FigmentError> { + fn validate( + &self, + _figment: &Figment, + ) -> Result<(), Box> { Ok(()) } @@ -27,7 +30,9 @@ pub trait ConfigurationSection: Sized + DeserializeOwned { /// # Errors /// /// Returns an error if the configuration could not be loaded - fn extract(figment: &Figment) -> Result { + fn extract( + figment: &Figment, + ) -> Result> { let this: Self = if let Some(path) = Self::PATH { figment.extract_inner(path)? } else { @@ -49,7 +54,9 @@ pub trait ConfigurationSectionExt: ConfigurationSection + Default { /// # Errors /// /// Returns an error if the configuration section is invalid. - fn extract_or_default(figment: &Figment) -> Result { + fn extract_or_default( + figment: &Figment, + ) -> Result> { let this: Self = if let Some(path) = Self::PATH { // If the configuration section is not present, we return the default value if !figment.contains(path) { diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index 46adc9911..ad86d1426 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-context" version.workspace = true diff --git a/crates/context/src/fmt.rs b/crates/context/src/fmt.rs index b074a9c34..579ea63a9 100644 --- a/crates/context/src/fmt.rs +++ b/crates/context/src/fmt.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use console::{Color, Style}; use opentelemetry::{ diff --git a/crates/context/src/future.rs b/crates/context/src/future.rs index 9e93af4fa..67c77fe67 100644 --- a/crates/context/src/future.rs +++ b/crates/context/src/future.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ pin::Pin, diff --git a/crates/context/src/layer.rs b/crates/context/src/layer.rs index 0ce6e3497..eb3f92bf6 100644 --- a/crates/context/src/layer.rs +++ b/crates/context/src/layer.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::borrow::Cow; diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index 655d407e9..c9644282d 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod fmt; mod future; diff --git a/crates/context/src/service.rs b/crates/context/src/service.rs index 98a1d1184..8d875cc0c 100644 --- a/crates/context/src/service.rs +++ b/crates/context/src/service.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ borrow::Cow, diff --git a/crates/data-model/Cargo.toml b/crates/data-model/Cargo.toml index c4e19fcdf..6474d46ae 100644 --- a/crates/data-model/Cargo.toml +++ b/crates/data-model/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-data-model" version.workspace = true diff --git a/crates/data-model/examples/ua-parser.rs b/crates/data-model/examples/ua-parser.rs index af2ec9179..98a4d7893 100644 --- a/crates/data-model/examples/ua-parser.rs +++ b/crates/data-model/examples/ua-parser.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_data_model::UserAgent; diff --git a/crates/data-model/src/compat/device.rs b/crates/data-model/src/compat/device.rs index fbe0d147a..e275b740f 100644 --- a/crates/data-model/src/compat/device.rs +++ b/crates/data-model/src/compat/device.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use oauth2_types::scope::ScopeToken; use rand::{ diff --git a/crates/data-model/src/compat/mod.rs b/crates/data-model/src/compat/mod.rs index c50d74261..be38154f3 100644 --- a/crates/data-model/src/compat/mod.rs +++ b/crates/data-model/src/compat/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use ulid::Ulid; diff --git a/crates/data-model/src/compat/session.rs b/crates/data-model/src/compat/session.rs index 91b48cea0..24a8e18a7 100644 --- a/crates/data-model/src/compat/session.rs +++ b/crates/data-model/src/compat/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/data-model/src/compat/sso_login.rs b/crates/data-model/src/compat/sso_login.rs index 448601a92..a42dfcb1d 100644 --- a/crates/data-model/src/compat/sso_login.rs +++ b/crates/data-model/src/compat/sso_login.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use serde::Serialize; diff --git a/crates/data-model/src/lib.rs b/crates/data-model/src/lib.rs index af4d0be37..f1b551891 100644 --- a/crates/data-model/src/lib.rs +++ b/crates/data-model/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::module_name_repetitions)] @@ -42,9 +42,10 @@ pub use self::{ UpstreamOAuthAuthorizationSession, UpstreamOAuthAuthorizationSessionState, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderImportAction, - UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderPkceMode, - UpstreamOAuthProviderResponseMode, UpstreamOAuthProviderSubjectPreference, - UpstreamOAuthProviderTokenAuthMethod, + UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderLocalpartPreference, + UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderOnConflict, + UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderResponseMode, + UpstreamOAuthProviderSubjectPreference, UpstreamOAuthProviderTokenAuthMethod, }, user_agent::{DeviceType, UserAgent}, users::{ diff --git a/crates/data-model/src/oauth2/authorization_grant.rs b/crates/data-model/src/oauth2/authorization_grant.rs index 1d71f0170..383701bcb 100644 --- a/crates/data-model/src/oauth2/authorization_grant.rs +++ b/crates/data-model/src/oauth2/authorization_grant.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use mas_iana::oauth::PkceCodeChallengeMethod; diff --git a/crates/data-model/src/oauth2/client.rs b/crates/data-model/src/oauth2/client.rs index c184d6fce..ce28445f3 100644 --- a/crates/data-model/src/oauth2/client.rs +++ b/crates/data-model/src/oauth2/client.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; diff --git a/crates/data-model/src/oauth2/device_code_grant.rs b/crates/data-model/src/oauth2/device_code_grant.rs index 794cc460b..aaf7df594 100644 --- a/crates/data-model/src/oauth2/device_code_grant.rs +++ b/crates/data-model/src/oauth2/device_code_grant.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/data-model/src/oauth2/mod.rs b/crates/data-model/src/oauth2/mod.rs index 0126392c1..6221a32fc 100644 --- a/crates/data-model/src/oauth2/mod.rs +++ b/crates/data-model/src/oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod authorization_grant; mod client; diff --git a/crates/data-model/src/oauth2/session.rs b/crates/data-model/src/oauth2/session.rs index 8a55aa863..c6c9346e9 100644 --- a/crates/data-model/src/oauth2/session.rs +++ b/crates/data-model/src/oauth2/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/data-model/src/policy_data.rs b/crates/data-model/src/policy_data.rs index 8836c2c0c..b732aca8d 100644 --- a/crates/data-model/src/policy_data.rs +++ b/crates/data-model/src/policy_data.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use serde::Serialize; diff --git a/crates/data-model/src/site_config.rs b/crates/data-model/src/site_config.rs index c441ba06f..ac0d7e6b8 100644 --- a/crates/data-model/src/site_config.rs +++ b/crates/data-model/src/site_config.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::Duration; use url::Url; diff --git a/crates/data-model/src/tokens.rs b/crates/data-model/src/tokens.rs index a98ba94e3..1ea5be6be 100644 --- a/crates/data-model/src/tokens.rs +++ b/crates/data-model/src/tokens.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use base64ct::{Base64UrlUnpadded, Encoding}; use chrono::{DateTime, Utc}; diff --git a/crates/data-model/src/upstream_oauth2/link.rs b/crates/data-model/src/upstream_oauth2/link.rs index e932b7384..421793ce2 100644 --- a/crates/data-model/src/upstream_oauth2/link.rs +++ b/crates/data-model/src/upstream_oauth2/link.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use serde::Serialize; diff --git a/crates/data-model/src/upstream_oauth2/mod.rs b/crates/data-model/src/upstream_oauth2/mod.rs index 8f4228839..563716568 100644 --- a/crates/data-model/src/upstream_oauth2/mod.rs +++ b/crates/data-model/src/upstream_oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod link; mod provider; @@ -15,7 +15,9 @@ pub use self::{ DiscoveryMode as UpstreamOAuthProviderDiscoveryMode, ImportAction as UpstreamOAuthProviderImportAction, ImportPreference as UpstreamOAuthProviderImportPreference, - PkceMode as UpstreamOAuthProviderPkceMode, + LocalpartPreference as UpstreamOAuthProviderLocalpartPreference, + OnBackchannelLogout as UpstreamOAuthProviderOnBackchannelLogout, + OnConflict as UpstreamOAuthProviderOnConflict, PkceMode as UpstreamOAuthProviderPkceMode, ResponseMode as UpstreamOAuthProviderResponseMode, SubjectPreference as UpstreamOAuthProviderSubjectPreference, TokenAuthMethod as UpstreamOAuthProviderTokenAuthMethod, UpstreamOAuthProvider, diff --git a/crates/data-model/src/upstream_oauth2/provider.rs b/crates/data-model/src/upstream_oauth2/provider.rs index 7362d807b..c54e40d15 100644 --- a/crates/data-model/src/upstream_oauth2/provider.rs +++ b/crates/data-model/src/upstream_oauth2/provider.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use mas_iana::jose::JsonWebSignatureAlg; @@ -216,6 +216,48 @@ impl std::str::FromStr for TokenAuthMethod { #[error("Invalid upstream OAuth 2.0 token auth method: {0}")] pub struct InvalidUpstreamOAuth2TokenAuthMethod(String); +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum OnBackchannelLogout { + DoNothing, + LogoutBrowserOnly, + LogoutAll, +} + +impl OnBackchannelLogout { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::DoNothing => "do_nothing", + Self::LogoutBrowserOnly => "logout_browser_only", + Self::LogoutAll => "logout_all", + } + } +} + +impl std::fmt::Display for OnBackchannelLogout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl std::str::FromStr for OnBackchannelLogout { + type Err = InvalidUpstreamOAuth2OnBackchannelLogout; + + fn from_str(s: &str) -> Result { + match s { + "do_nothing" => Ok(Self::DoNothing), + "logout_browser_only" => Ok(Self::LogoutBrowserOnly), + "logout_all" => Ok(Self::LogoutAll), + s => Err(InvalidUpstreamOAuth2OnBackchannelLogout(s.to_owned())), + } + } +} + +#[derive(Debug, Clone, Error)] +#[error("Invalid upstream OAuth 2.0 'on backchannel logout': {0}")] +pub struct InvalidUpstreamOAuth2OnBackchannelLogout(String); + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct UpstreamOAuthProvider { pub id: Ulid, @@ -242,6 +284,7 @@ pub struct UpstreamOAuthProvider { pub claims_imports: ClaimsImports, pub additional_authorization_parameters: Vec<(String, String)>, pub forward_login_hint: bool, + pub on_backchannel_logout: OnBackchannelLogout, } impl PartialOrd for UpstreamOAuthProvider { @@ -270,7 +313,7 @@ pub struct ClaimsImports { pub subject: SubjectPreference, #[serde(default)] - pub localpart: ImportPreference, + pub localpart: LocalpartPreference, #[serde(default)] pub displayname: ImportPreference, @@ -289,6 +332,26 @@ pub struct SubjectPreference { pub template: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct LocalpartPreference { + #[serde(default)] + pub action: ImportAction, + + #[serde(default)] + pub template: Option, + + #[serde(default)] + pub on_conflict: OnConflict, +} + +impl std::ops::Deref for LocalpartPreference { + type Target = ImportAction; + + fn deref(&self) -> &Self::Target { + &self.action + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ImportPreference { #[serde(default)] @@ -325,7 +388,7 @@ pub enum ImportAction { impl ImportAction { #[must_use] - pub fn is_forced(&self) -> bool { + pub fn is_forced_or_required(&self) -> bool { matches!(self, Self::Force | Self::Require) } @@ -348,3 +411,15 @@ impl ImportAction { } } } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum OnConflict { + /// Fails the upstream OAuth 2.0 login + #[default] + Fail, + + /// Adds the upstream account link, regardless of whether there is an + /// existing link or not + Add, +} diff --git a/crates/data-model/src/upstream_oauth2/session.rs b/crates/data-model/src/upstream_oauth2/session.rs index c5b45234c..e7dad7132 100644 --- a/crates/data-model/src/upstream_oauth2/session.rs +++ b/crates/data-model/src/upstream_oauth2/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use serde::Serialize; @@ -19,6 +19,7 @@ pub enum UpstreamOAuthAuthorizationSessionState { completed_at: DateTime, link_id: Ulid, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, }, @@ -27,6 +28,7 @@ pub enum UpstreamOAuthAuthorizationSessionState { consumed_at: DateTime, link_id: Ulid, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, }, @@ -35,6 +37,7 @@ pub enum UpstreamOAuthAuthorizationSessionState { consumed_at: Option>, unlinked_at: DateTime, id_token: Option, + id_token_claims: Option, }, } @@ -52,6 +55,7 @@ impl UpstreamOAuthAuthorizationSessionState { completed_at: DateTime, link: &UpstreamOAuthLink, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, ) -> Result { @@ -60,6 +64,7 @@ impl UpstreamOAuthAuthorizationSessionState { completed_at, link_id: link.id, id_token, + id_token_claims, extra_callback_parameters, userinfo, }), @@ -83,6 +88,7 @@ impl UpstreamOAuthAuthorizationSessionState { completed_at, link_id, id_token, + id_token_claims, extra_callback_parameters, userinfo, } => Ok(Self::Consumed { @@ -90,6 +96,7 @@ impl UpstreamOAuthAuthorizationSessionState { link_id, consumed_at, id_token, + id_token_claims, extra_callback_parameters, userinfo, }), @@ -146,6 +153,29 @@ impl UpstreamOAuthAuthorizationSessionState { } } + /// Get the ID token claims for the upstream OAuth 2.0 authorization + /// session. + /// + /// Returns `None` if the upstream OAuth 2.0 authorization session state is + /// not [`Pending`]. + /// + /// [`Pending`]: UpstreamOAuthAuthorizationSessionState::Pending + #[must_use] + pub fn id_token_claims(&self) -> Option<&serde_json::Value> { + match self { + Self::Pending => None, + Self::Completed { + id_token_claims, .. + } + | Self::Consumed { + id_token_claims, .. + } + | Self::Unlinked { + id_token_claims, .. + } => id_token_claims.as_ref(), + } + } + /// Get the extra query parameters that were sent to the upstream provider. /// /// Returns `None` if the upstream OAuth 2.0 authorization session state is @@ -277,6 +307,7 @@ impl UpstreamOAuthAuthorizationSession { completed_at: DateTime, link: &UpstreamOAuthLink, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, ) -> Result { @@ -284,6 +315,7 @@ impl UpstreamOAuthAuthorizationSession { completed_at, link, id_token, + id_token_claims, extra_callback_parameters, userinfo, )?; diff --git a/crates/data-model/src/user_agent.rs b/crates/data-model/src/user_agent.rs index 2ac4b06bd..4efb4ff9f 100644 --- a/crates/data-model/src/user_agent.rs +++ b/crates/data-model/src/user_agent.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index fc6ed2695..c2addf8a3 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index 7b8ff94c4..eba4ea6fa 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-email" version.workspace = true diff --git a/crates/email/src/lib.rs b/crates/email/src/lib.rs index 49e940638..ee731f744 100644 --- a/crates/email/src/lib.rs +++ b/crates/email/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Helps sending emails to users, with different email backends diff --git a/crates/email/src/mailer.rs b/crates/email/src/mailer.rs index 443859a3b..d31fe96ab 100644 --- a/crates/email/src/mailer.rs +++ b/crates/email/src/mailer.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Send emails to users diff --git a/crates/email/src/transport.rs b/crates/email/src/transport.rs index 21291a1b1..9161d76e5 100644 --- a/crates/email/src/transport.rs +++ b/crates/email/src/transport.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Email transport backends diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 4bda28ced..57f391854 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-handlers" version.workspace = true @@ -67,6 +72,7 @@ mas-axum-utils.workspace = true mas-config.workspace = true mas-context.workspace = true mas-data-model.workspace = true +mas-email.workspace = true mas-http.workspace = true mas-i18n.workspace = true mas-iana.workspace = true @@ -78,6 +84,7 @@ mas-policy.workspace = true mas-router.workspace = true mas-storage.workspace = true mas-storage-pg.workspace = true +mas-tasks.workspace = true mas-templates.workspace = true oauth2-types.workspace = true zxcvbn.workspace = true diff --git a/crates/handlers/src/activity_tracker/bound.rs b/crates/handlers/src/activity_tracker/bound.rs index c4e2f7cce..86286ebf8 100644 --- a/crates/handlers/src/activity_tracker/bound.rs +++ b/crates/handlers/src/activity_tracker/bound.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/handlers/src/activity_tracker/mod.rs b/crates/handlers/src/activity_tracker/mod.rs index 56785e236..6f8bf41d6 100644 --- a/crates/handlers/src/activity_tracker/mod.rs +++ b/crates/handlers/src/activity_tracker/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod bound; mod worker; diff --git a/crates/handlers/src/activity_tracker/worker.rs b/crates/handlers/src/activity_tracker/worker.rs index 4787964ee..46cc84ccd 100644 --- a/crates/handlers/src/activity_tracker/worker.rs +++ b/crates/handlers/src/activity_tracker/worker.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, net::IpAddr}; diff --git a/crates/handlers/src/admin/call_context.rs b/crates/handlers/src/admin/call_context.rs index 95340b160..d5a5f4b94 100644 --- a/crates/handlers/src/admin/call_context.rs +++ b/crates/handlers/src/admin/call_context.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::convert::Infallible; diff --git a/crates/handlers/src/admin/mod.rs b/crates/handlers/src/admin/mod.rs index 6890848ac..c0a0cf9ac 100644 --- a/crates/handlers/src/admin/mod.rs +++ b/crates/handlers/src/admin/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index ceb19692a..62065dfe4 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/handlers/src/admin/params.rs b/crates/handlers/src/admin/params.rs index 749bc62b3..633917d9a 100644 --- a/crates/handlers/src/admin/params.rs +++ b/crates/handlers/src/admin/params.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // Generated code from schemars violates this rule #![allow(clippy::str_to_string)] diff --git a/crates/handlers/src/admin/response.rs b/crates/handlers/src/admin/response.rs index 753260961..19f0e8040 100644 --- a/crates/handlers/src/admin/response.rs +++ b/crates/handlers/src/admin/response.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::module_name_repetitions)] diff --git a/crates/handlers/src/admin/schema.rs b/crates/handlers/src/admin/schema.rs index 99f1c20b7..068c68977 100644 --- a/crates/handlers/src/admin/schema.rs +++ b/crates/handlers/src/admin/schema.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Common schema definitions diff --git a/crates/handlers/src/admin/v1/compat_sessions/get.rs b/crates/handlers/src/admin/v1/compat_sessions/get.rs index 3d471d0ce..c77432d07 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/get.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/get.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index adf15d190..debb2a304 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{ diff --git a/crates/handlers/src/admin/v1/compat_sessions/mod.rs b/crates/handlers/src/admin/v1/compat_sessions/mod.rs index 23c05c416..18ffe5af6 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/mod.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod get; mod list; diff --git a/crates/handlers/src/admin/v1/mod.rs b/crates/handlers/src/admin/v1/mod.rs index 02586368b..1d167e98a 100644 --- a/crates/handlers/src/admin/v1/mod.rs +++ b/crates/handlers/src/admin/v1/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; @@ -27,7 +27,6 @@ mod user_registration_tokens; mod user_sessions; mod users; -#[allow(clippy::too_many_lines)] pub fn router() -> ApiRouter where S: Clone + Send + Sync + 'static, @@ -94,6 +93,10 @@ where "/users/{id}/deactivate", post_with(self::users::deactivate, self::users::deactivate_doc), ) + .api_route( + "/users/{id}/reactivate", + post_with(self::users::reactivate, self::users::reactivate_doc), + ) .api_route( "/users/{id}/lock", post_with(self::users::lock, self::users::lock_doc), diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/get.rs b/crates/handlers/src/admin/v1/oauth2_sessions/get.rs index 88f46ecff..653bb69b2 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/get.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/get.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs index 49b429243..52b597edc 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::str::FromStr; diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs b/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs index 84a9efde5..9b6272cef 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod get; mod list; diff --git a/crates/handlers/src/admin/v1/policy_data/get.rs b/crates/handlers/src/admin/v1/policy_data/get.rs index 51d8c7849..1ba0517fc 100644 --- a/crates/handlers/src/admin/v1/policy_data/get.rs +++ b/crates/handlers/src/admin/v1/policy_data/get.rs @@ -1,6 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/policy_data/get_latest.rs b/crates/handlers/src/admin/v1/policy_data/get_latest.rs index f217b30dc..102c578f1 100644 --- a/crates/handlers/src/admin/v1/policy_data/get_latest.rs +++ b/crates/handlers/src/admin/v1/policy_data/get_latest.rs @@ -1,6 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/policy_data/mod.rs b/crates/handlers/src/admin/v1/policy_data/mod.rs index 9143a2e11..f8952e711 100644 --- a/crates/handlers/src/admin/v1/policy_data/mod.rs +++ b/crates/handlers/src/admin/v1/policy_data/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod get; mod get_latest; diff --git a/crates/handlers/src/admin/v1/policy_data/set.rs b/crates/handlers/src/admin/v1/policy_data/set.rs index bc28e96e3..93260f9ac 100644 --- a/crates/handlers/src/admin/v1/policy_data/set.rs +++ b/crates/handlers/src/admin/v1/policy_data/set.rs @@ -1,6 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs index 3cdbc783f..ba59d4419 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/add.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs index 403c1bc33..3e87109b5 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; @@ -126,7 +126,7 @@ mod tests { let session = repo .upstream_oauth_session() - .complete_with_link(&state.clock, session, &link, None, None, None) + .complete_with_link(&state.clock, session, &link, None, None, None, None) .await .unwrap(); diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs index 4fd5158d3..483d90891 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/get.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs index e46b85820..59efe6541 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{ diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs index 6696a7109..3433aa3ca 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod add; mod delete; @@ -19,7 +19,8 @@ pub use self::{ mod test_utils { use mas_data_model::{ UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, - UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod, + UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode, + UpstreamOAuthProviderTokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use mas_storage::upstream_oauth2::UpstreamOAuthProviderParams; @@ -49,6 +50,7 @@ mod test_utils { additional_authorization_parameters: Vec::new(), forward_login_hint: false, ui_order: 0, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, } } } diff --git a/crates/handlers/src/admin/v1/user_emails/add.rs b/crates/handlers/src/admin/v1/user_emails/add.rs index f3a39e20a..55038a591 100644 --- a/crates/handlers/src/admin/v1/user_emails/add.rs +++ b/crates/handlers/src/admin/v1/user_emails/add.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::str::FromStr as _; diff --git a/crates/handlers/src/admin/v1/user_emails/delete.rs b/crates/handlers/src/admin/v1/user_emails/delete.rs index ad7df7acb..844edbff2 100644 --- a/crates/handlers/src/admin/v1/user_emails/delete.rs +++ b/crates/handlers/src/admin/v1/user_emails/delete.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_emails/get.rs b/crates/handlers/src/admin/v1/user_emails/get.rs index 9232b0663..826cb8c25 100644 --- a/crates/handlers/src/admin/v1/user_emails/get.rs +++ b/crates/handlers/src/admin/v1/user_emails/get.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_emails/list.rs b/crates/handlers/src/admin/v1/user_emails/list.rs index f7adb23da..92dfe12c2 100644 --- a/crates/handlers/src/admin/v1/user_emails/list.rs +++ b/crates/handlers/src/admin/v1/user_emails/list.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{ diff --git a/crates/handlers/src/admin/v1/user_emails/mod.rs b/crates/handlers/src/admin/v1/user_emails/mod.rs index 136c132ba..38f3ec989 100644 --- a/crates/handlers/src/admin/v1/user_emails/mod.rs +++ b/crates/handlers/src/admin/v1/user_emails/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod add; mod delete; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/add.rs b/crates/handlers/src/admin/v1/user_registration_tokens/add.rs index afa409a67..7bc2b0593 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/add.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/add.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/get.rs b/crates/handlers/src/admin/v1/user_registration_tokens/get.rs index 833e3b17c..187c19032 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/get.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/get.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs index 85a1a1945..f9b0e714c 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{ diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/mod.rs b/crates/handlers/src/admin/v1/user_registration_tokens/mod.rs index 3d61e10e6..42d16af7b 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/mod.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/mod.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod add; mod get; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs b/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs index e649cfef8..9e442640f 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/revoke.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs b/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs index 53cbfcf95..212b7cdf9 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/unrevoke.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/update.rs b/crates/handlers/src/admin/v1/user_registration_tokens/update.rs index 444c7ae6b..d5eaa7342 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/update.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/update.rs @@ -1,7 +1,8 @@ +// Copyright 2025 New Vector Ltd. // Copyright 2025 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_sessions/get.rs b/crates/handlers/src/admin/v1/user_sessions/get.rs index a59b10d0e..0a65c80c1 100644 --- a/crates/handlers/src/admin/v1/user_sessions/get.rs +++ b/crates/handlers/src/admin/v1/user_sessions/get.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/user_sessions/list.rs b/crates/handlers/src/admin/v1/user_sessions/list.rs index a04bf057f..28a52edf2 100644 --- a/crates/handlers/src/admin/v1/user_sessions/list.rs +++ b/crates/handlers/src/admin/v1/user_sessions/list.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{ diff --git a/crates/handlers/src/admin/v1/user_sessions/mod.rs b/crates/handlers/src/admin/v1/user_sessions/mod.rs index 23c05c416..18ffe5af6 100644 --- a/crates/handlers/src/admin/v1/user_sessions/mod.rs +++ b/crates/handlers/src/admin/v1/user_sessions/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod get; mod list; diff --git a/crates/handlers/src/admin/v1/users/add.rs b/crates/handlers/src/admin/v1/users/add.rs index 9867b06ec..d67737526 100644 --- a/crates/handlers/src/admin/v1/users/add.rs +++ b/crates/handlers/src/admin/v1/users/add.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; @@ -10,11 +10,8 @@ use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; use mas_axum_utils::record_error; -use mas_matrix::HomeserverConnection; -use mas_storage::{ - BoxRng, - queue::{ProvisionUserJob, QueueJobRepositoryExt as _}, -}; +use mas_matrix::{HomeserverConnection, ProvisionRequest}; +use mas_storage::BoxRng; use schemars::JsonSchema; use serde::Deserialize; use tracing::warn; @@ -168,9 +165,10 @@ pub async fn handler( let user = repo.user().add(&mut rng, &clock, params.username).await?; - repo.queue_job() - .schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user)) - .await?; + homeserver + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) + .await + .map_err(RouteError::Homeserver)?; repo.save().await?; @@ -183,6 +181,7 @@ pub async fn handler( #[cfg(test)] mod tests { use hyper::{Request, StatusCode}; + use mas_matrix::HomeserverConnection; use mas_storage::{RepositoryAccess, user::UserRepository}; use sqlx::PgPool; @@ -218,6 +217,10 @@ mod tests { .unwrap(); assert_eq!(user.username, "alice"); + + // Check that the user was created on the homeserver + let result = state.homeserver_connection.query_user("alice").await; + assert!(result.is_ok()); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/admin/v1/users/by_username.rs b/crates/handlers/src/admin/v1/users/by_username.rs index 98ddb8b3c..2ba122039 100644 --- a/crates/handlers/src/admin/v1/users/by_username.rs +++ b/crates/handlers/src/admin/v1/users/by_username.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, extract::Path, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index fad2f5257..25fae6a3c 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; @@ -12,6 +12,8 @@ use mas_storage::{ BoxRng, queue::{DeactivateUserJob, QueueJobRepositoryExt as _}, }; +use schemars::JsonSchema; +use serde::Deserialize; use tracing::info; use ulid::Ulid; @@ -49,18 +51,40 @@ impl IntoResponse for RouteError { } } -pub fn doc(operation: TransformOperation) -> TransformOperation { +/// # JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint +#[derive(Default, Deserialize, JsonSchema)] +#[serde(rename = "DeactivateUserRequest")] +pub struct Request { + /// Whether to skip requesting the homeserver to GDPR-erase the user upon + /// deactivation. + #[serde(default)] + skip_erase: bool, +} + +pub fn doc(mut operation: TransformOperation) -> TransformOperation { + operation + .inner_mut() + .request_body + .as_mut() + .unwrap() + .as_item_mut() + .unwrap() + .required = false; + operation .id("deactivateUser") .summary("Deactivate a user") - .description("Calling this endpoint will lock and deactivate the user, preventing them from doing any action. -This invalidates any existing session, and will ask the homeserver to make them leave all rooms.") + .description( + "Calling this endpoint will deactivate the user, preventing them from doing any action. +This invalidates any existing session, and will ask the homeserver to make them leave all rooms.", + ) .tag("user") .response_with::<200, Json>, _>(|t| { // In the samples, the third user is the one locked let [_alice, _bob, charlie, ..] = User::samples(); let id = charlie.id(); - let response = SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate")); + let response = + SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate")); t.description("User was deactivated").example(response) }) .response_with::<404, RouteError, _>(|t| { @@ -76,21 +100,25 @@ pub async fn handler( }: CallContext, NoApi(mut rng): NoApi, id: UlidPathParam, + body: Option>, ) -> Result>, RouteError> { + let Json(params) = body.unwrap_or_default(); let id = *id; - let mut user = repo + let user = repo .user() .lookup(id) .await? .ok_or(RouteError::NotFound(id))?; - if user.locked_at.is_none() { - user = repo.user().lock(&clock, user).await?; - } + let user = repo.user().deactivate(&clock, user).await?; info!(%user.id, "Scheduling deactivation of user"); repo.queue_job() - .schedule_job(&mut rng, &clock, DeactivateUserJob::new(&user, true)) + .schedule_job( + &mut rng, + &clock, + DeactivateUserJob::new(&user, !params.skip_erase), + ) .await?; repo.save().await?; @@ -105,13 +133,13 @@ pub async fn handler( mod tests { use chrono::Duration; use hyper::{Request, StatusCode}; + use insta::{allow_duplicates, assert_json_snapshot}; use mas_storage::{Clock, RepositoryAccess, user::UserRepository}; use sqlx::{PgPool, types::Json}; use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_deactivate_user(pool: PgPool) { + async fn test_deactivate_user_helper(pool: PgPool, skip_erase: Option) { setup(); let mut state = TestState::from_pool(pool.clone()).await.unwrap(); let token = state.token_with_scope("urn:mas:admin").await; @@ -124,17 +152,28 @@ mod tests { .unwrap(); repo.save().await.unwrap(); - let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)) - .bearer(&token) - .empty(); + let request = + Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)).bearer(&token); + let request = match skip_erase { + None => request.empty(), + Some(skip_erase) => request.json(serde_json::json!({ + "skip_erase": skip_erase, + })), + }; let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - // The locked_at timestamp should be the same as the current time + // The deactivated_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["deactivated_at"], + serde_json::json!(state.clock.now()) + ); + + // Deactivating the user should not lock it assert_eq!( body["data"]["attributes"]["locked_at"], - serde_json::json!(state.clock.now()) + serde_json::Value::Null ); // It should have scheduled a deactivation job for the user @@ -146,6 +185,52 @@ mod tests { .await .expect("Deactivation job to be scheduled"); assert_eq!(job["user_id"], serde_json::json!(user.id)); + assert_eq!( + job["hs_erase"], + serde_json::json!(!skip_erase.unwrap_or(false)) + ); + + // Make sure to run the jobs in the queue + state.run_jobs_in_queue().await; + + let request = Request::get(format!("/api/admin/v1/users/{}", user.id)) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + allow_duplicates!(assert_json_snapshot!(body, @r#" + { + "data": { + "type": "user", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "username": "alice", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": null, + "deactivated_at": "2022-01-16T14:40:00Z", + "admin": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + } + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + "#)); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_deactivate_user(pool: PgPool) { + test_deactivate_user_helper(pool, Option::None).await; + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_deactivate_user_skip_erase(pool: PgPool) { + test_deactivate_user_helper(pool, Option::Some(true)).await; } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] @@ -173,21 +258,49 @@ mod tests { response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - // The locked_at timestamp should be different from the current time - assert_ne!( - body["data"]["attributes"]["locked_at"], + // The deactivated_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["deactivated_at"], serde_json::json!(state.clock.now()) ); - // It should have scheduled a deactivation job for the user - // XXX: we don't have a good way to look for the deactivation job - let job: Json = sqlx::query_scalar( - "SELECT payload FROM queue_jobs WHERE queue_name = 'deactivate-user'", - ) - .fetch_one(&pool) - .await - .expect("Deactivation job to be scheduled"); - assert_eq!(job["user_id"], serde_json::json!(user.id)); + // The deactivated_at timestamp should be different from the locked_at timestamp + assert_ne!( + body["data"]["attributes"]["deactivated_at"], + body["data"]["attributes"]["locked_at"], + ); + + // Make sure to run the jobs in the queue + state.run_jobs_in_queue().await; + + let request = Request::get(format!("/api/admin/v1/users/{}", user.id)) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + assert_json_snapshot!(body, @r#" + { + "data": { + "type": "user", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "username": "alice", + "created_at": "2022-01-16T14:40:00Z", + "locked_at": "2022-01-16T14:40:00Z", + "deactivated_at": "2022-01-16T14:41:00Z", + "admin": false + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + } + }, + "links": { + "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + "#); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/admin/v1/users/get.rs b/crates/handlers/src/admin/v1/users/get.rs index 59d221fbb..afc177011 100644 --- a/crates/handlers/src/admin/v1/users/get.rs +++ b/crates/handlers/src/admin/v1/users/get.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/users/list.rs b/crates/handlers/src/admin/v1/users/list.rs index 9bc04ab4a..021e39f37 100644 --- a/crates/handlers/src/admin/v1/users/list.rs +++ b/crates/handlers/src/admin/v1/users/list.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{ diff --git a/crates/handlers/src/admin/v1/users/lock.rs b/crates/handlers/src/admin/v1/users/lock.rs index 13ffdc071..ec8159532 100644 --- a/crates/handlers/src/admin/v1/users/lock.rs +++ b/crates/handlers/src/admin/v1/users/lock.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; @@ -72,15 +72,13 @@ pub async fn handler( id: UlidPathParam, ) -> Result>, RouteError> { let id = *id; - let mut user = repo + let user = repo .user() .lookup(id) .await? .ok_or(RouteError::NotFound(id))?; - if user.locked_at.is_none() { - user = repo.user().lock(&clock, user).await?; - } + let user = repo.user().lock(&clock, user).await?; repo.save().await?; @@ -157,6 +155,10 @@ mod tests { body["data"]["attributes"]["locked_at"], serde_json::json!(state.clock.now()) ); + assert_ne!( + body["data"]["attributes"]["locked_at"], + serde_json::Value::Null + ); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/admin/v1/users/mod.rs b/crates/handlers/src/admin/v1/users/mod.rs index ff610f167..37484b75b 100644 --- a/crates/handlers/src/admin/v1/users/mod.rs +++ b/crates/handlers/src/admin/v1/users/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod add; mod by_username; @@ -10,6 +10,7 @@ mod deactivate; mod get; mod list; mod lock; +mod reactivate; mod set_admin; mod set_password; mod unlock; @@ -21,6 +22,7 @@ pub use self::{ get::{doc as get_doc, handler as get}, list::{doc as list_doc, handler as list}, lock::{doc as lock_doc, handler as lock}, + reactivate::{doc as reactivate_doc, handler as reactivate}, set_admin::{doc as set_admin_doc, handler as set_admin}, set_password::{doc as set_password_doc, handler as set_password}, unlock::{doc as unlock_doc, handler as unlock}, diff --git a/crates/handlers/src/admin/v1/users/reactivate.rs b/crates/handlers/src/admin/v1/users/reactivate.rs new file mode 100644 index 000000000..0be687a39 --- /dev/null +++ b/crates/handlers/src/admin/v1/users/reactivate.rs @@ -0,0 +1,221 @@ +// 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::sync::Arc; + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{Json, extract::State, response::IntoResponse}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_matrix::HomeserverConnection; +use ulid::Ulid; + +use crate::{ + admin::{ + call_context::CallContext, + model::{Resource, User}, + params::UlidPathParam, + response::{ErrorResponse, SingleResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error(transparent)] + Homeserver(anyhow::Error), + + #[error("User ID {0} not found")] + NotFound(Ulid), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_)); + let status = match self { + Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NotFound(_) => StatusCode::NOT_FOUND, + }; + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("reactivateUser") + .summary("Reactivate a user") + .description("Calling this endpoint will reactivate a deactivated user. +This DOES NOT unlock a locked user, which is still prevented from doing any action until it is explicitly unlocked.") + .tag("user") + .response_with::<200, Json>, _>(|t| { + // In the samples, the third user is the one locked + let [sample, ..] = User::samples(); + let id = sample.id(); + let response = SingleResponse::new(sample, format!("/api/admin/v1/users/{id}/reactivate")); + t.description("User was reactivated").example(response) + }) + .response_with::<404, RouteError, _>(|t| { + let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil())); + t.description("User ID not found").example(response) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.users.reactivate", skip_all)] +pub async fn handler( + CallContext { mut repo, .. }: CallContext, + State(homeserver): State>, + id: UlidPathParam, +) -> Result>, RouteError> { + let id = *id; + let user = repo + .user() + .lookup(id) + .await? + .ok_or(RouteError::NotFound(id))?; + + // Call the homeserver synchronously to reactivate the user + homeserver + .reactivate_user(&user.username) + .await + .map_err(RouteError::Homeserver)?; + + // Now reactivate the user in our database + let user = repo.user().reactivate(user).await?; + + repo.save().await?; + + Ok(Json(SingleResponse::new( + User::from(user), + format!("/api/admin/v1/users/{id}/reactivate"), + ))) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use mas_matrix::{HomeserverConnection, ProvisionRequest}; + use mas_storage::{Clock, RepositoryAccess, user::UserRepository}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_reactivate_deactivated_user(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool.clone()).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut state.rng(), &state.clock, "alice".to_owned()) + .await + .unwrap(); + let user = repo.user().lock(&state.clock, user).await.unwrap(); + let user = repo.user().deactivate(&state.clock, user).await.unwrap(); + repo.save().await.unwrap(); + + // Provision and immediately deactivate the user on the homeserver, + // because this endpoint will try to reactivate it + state + .homeserver_connection + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) + .await + .unwrap(); + state + .homeserver_connection + .delete_user(&user.username, true) + .await + .unwrap(); + + // The user should be deactivated on the homeserver + 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)) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + // The user should remain locked after being reactivated + assert_eq!( + body["data"]["attributes"]["locked_at"], + serde_json::json!(state.clock.now()) + ); + assert_eq!( + body["data"]["attributes"]["deactivated_at"], + serde_json::Value::Null, + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_reactivate_active_user(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool.clone()).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut state.rng(), &state.clock, "alice".to_owned()) + .await + .unwrap(); + repo.save().await.unwrap(); + + // Provision the user on the homeserver + state + .homeserver_connection + .provision_user(&ProvisionRequest::new(&user.username, &user.sub)) + .await + .unwrap(); + + let request = Request::post(format!("/api/admin/v1/users/{}/reactivate", user.id)) + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json(); + + assert_eq!( + body["data"]["attributes"]["locked_at"], + serde_json::Value::Null + ); + assert_eq!( + body["data"]["attributes"]["deactivated_at"], + serde_json::Value::Null + ); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_reactivate_unknown_user(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = Request::post("/api/admin/v1/users/01040G2081040G2081040G2081/reactivate") + .bearer(&token) + .empty(); + let response = state.request(request).await; + response.assert_status(StatusCode::NOT_FOUND); + let body: serde_json::Value = response.json(); + assert_eq!( + body["errors"][0]["title"], + "User ID 01040G2081040G2081040G2081 not found" + ); + } +} diff --git a/crates/handlers/src/admin/v1/users/set_admin.rs b/crates/handlers/src/admin/v1/users/set_admin.rs index 72df1f71b..455fa7988 100644 --- a/crates/handlers/src/admin/v1/users/set_admin.rs +++ b/crates/handlers/src/admin/v1/users/set_admin.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; use axum::{Json, response::IntoResponse}; diff --git a/crates/handlers/src/admin/v1/users/set_password.rs b/crates/handlers/src/admin/v1/users/set_password.rs index 06797c3f8..633d72e67 100644 --- a/crates/handlers/src/admin/v1/users/set_password.rs +++ b/crates/handlers/src/admin/v1/users/set_password.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; @@ -145,7 +145,7 @@ mod tests { use zeroize::Zeroizing; use crate::{ - passwords::PasswordManager, + passwords::{PasswordManager, PasswordVerificationResult}, test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}, }; @@ -185,7 +185,7 @@ mod tests { let mut repo = state.repository().await.unwrap(); let user_password = repo.user_password().active(&user).await.unwrap().unwrap(); let password = Zeroizing::new(String::from("this is a good enough password")); - state + let res = state .password_manager .verify( user_password.version, @@ -194,6 +194,7 @@ mod tests { ) .await .unwrap(); + assert_eq!(res, PasswordVerificationResult::Success(())); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] @@ -244,7 +245,7 @@ mod tests { let mut repo = state.repository().await.unwrap(); let user_password = repo.user_password().active(&user).await.unwrap().unwrap(); let password = Zeroizing::new("password".to_owned()); - state + let res = state .password_manager .verify( user_password.version, @@ -253,6 +254,7 @@ mod tests { ) .await .unwrap(); + assert_eq!(res, PasswordVerificationResult::Success(())); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/admin/v1/users/unlock.rs b/crates/handlers/src/admin/v1/users/unlock.rs index e1811378c..944dd77f4 100644 --- a/crates/handlers/src/admin/v1/users/unlock.rs +++ b/crates/handlers/src/admin/v1/users/unlock.rs @@ -1,16 +1,13 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. - -use std::sync::Arc; +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use aide::{OperationIo, transform::TransformOperation}; -use axum::{Json, extract::State, response::IntoResponse}; +use axum::{Json, response::IntoResponse}; use hyper::StatusCode; use mas_axum_utils::record_error; -use mas_matrix::HomeserverConnection; use ulid::Ulid; use crate::{ @@ -29,9 +26,6 @@ pub enum RouteError { #[error(transparent)] Internal(Box), - #[error(transparent)] - Homeserver(anyhow::Error), - #[error("User ID {0} not found")] NotFound(Ulid), } @@ -41,9 +35,9 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); - let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_)); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { - Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; (status, sentry_event_id, Json(error)).into_response() @@ -54,6 +48,8 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { operation .id("unlockUser") .summary("Unlock a user") + .description("Calling this endpoint will lift restrictions on user actions that had imposed by locking. +This DOES NOT reactivate a deactivated user, which will remain unavailable until it is explicitly reactivated.") .tag("user") .response_with::<200, Json>, _>(|t| { // In the samples, the third user is the one locked @@ -71,7 +67,6 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.users.unlock", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - State(homeserver): State>, id: UlidPathParam, ) -> Result>, RouteError> { let id = *id; @@ -81,14 +76,6 @@ pub async fn handler( .await? .ok_or(RouteError::NotFound(id))?; - // Call the homeserver synchronously to unlock the user - let mxid = homeserver.mxid(&user.username); - homeserver - .reactivate_user(&mxid) - .await - .map_err(RouteError::Homeserver)?; - - // Now unlock the user in our database let user = repo.user().unlock(user).await?; repo.save().await?; @@ -103,7 +90,7 @@ pub async fn handler( mod tests { use hyper::{Request, StatusCode}; use mas_matrix::{HomeserverConnection, ProvisionRequest}; - use mas_storage::{RepositoryAccess, user::UserRepository}; + use mas_storage::{Clock, RepositoryAccess, user::UserRepository}; use sqlx::PgPool; use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; @@ -125,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(); @@ -141,7 +127,7 @@ mod tests { assert_eq!( body["data"]["attributes"]["locked_at"], - serde_json::json!(null) + serde_json::Value::Null ); } @@ -158,24 +144,28 @@ mod tests { .await .unwrap(); let user = repo.user().lock(&state.clock, user).await.unwrap(); + let user = repo.user().deactivate(&state.clock, user).await.unwrap(); 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)) @@ -187,11 +177,19 @@ mod tests { assert_eq!( body["data"]["attributes"]["locked_at"], - serde_json::json!(null) + serde_json::Value::Null ); - // The user should be reactivated on the homeserver - let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap(); - assert!(!mx_user.deactivated); + // The user should remain deactivated + assert_eq!( + body["data"]["attributes"]["deactivated_at"], + serde_json::json!(state.clock.now()) + ); + let mx_user = state + .homeserver_connection + .query_user(&user.username) + .await + .unwrap(); + assert!(mx_user.deactivated); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/bin/api-schema.rs b/crates/handlers/src/bin/api-schema.rs index a3f59c4c6..ee736de2a 100644 --- a/crates/handlers/src/bin/api-schema.rs +++ b/crates/handlers/src/bin/api-schema.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![forbid(unsafe_code)] #![deny( diff --git a/crates/handlers/src/bin/graphql-schema.rs b/crates/handlers/src/bin/graphql-schema.rs index d2ac8a642..45bdcc175 100644 --- a/crates/handlers/src/bin/graphql-schema.rs +++ b/crates/handlers/src/bin/graphql-schema.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![forbid(unsafe_code)] #![deny( diff --git a/crates/handlers/src/captcha.rs b/crates/handlers/src/captcha.rs index 740995145..c206df5cf 100644 --- a/crates/handlers/src/captcha.rs +++ b/crates/handlers/src/captcha.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 32bdfacd3..d75b4f8bd 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock}; @@ -32,7 +32,8 @@ use zeroize::Zeroizing; use super::{MatrixError, MatrixJsonBody}; use crate::{ BoundActivityTracker, Limiter, METER, RequesterFingerprint, impl_from_error_for_route, - passwords::PasswordManager, rate_limit::PasswordCheckLimitedError, + passwords::{PasswordManager, PasswordVerificationResult}, + rate_limit::PasswordCheckLimitedError, }; static LOGIN_COUNTER: LazyLock> = LazyLock::new(|| { @@ -193,7 +194,7 @@ pub enum RouteError { NoPassword, #[error("password verification failed")] - PasswordVerificationFailed(#[source] anyhow::Error), + PasswordMismatch, #[error("request rate limited")] RateLimited(#[from] PasswordCheckLimitedError), @@ -204,12 +205,21 @@ pub enum RouteError { #[error("invalid login token")] InvalidLoginToken, + #[error("user is locked")] + UserLocked, + #[error("failed to provision device")] ProvisionDeviceFailed(#[source] anyhow::Error), } impl_from_error_for_route!(mas_storage::RepositoryError); +impl From for RouteError { + fn from(err: anyhow::Error) -> Self { + Self::Internal(err.into()) + } +} + impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let sentry_event_id = @@ -241,13 +251,11 @@ impl IntoResponse for RouteError { error: "Missing property 'identifier", status: StatusCode::BAD_REQUEST, }, - Self::UserNotFound | Self::NoPassword | Self::PasswordVerificationFailed(_) => { - MatrixError { - errcode: "M_FORBIDDEN", - error: "Invalid username/password", - status: StatusCode::FORBIDDEN, - } - } + Self::UserNotFound | Self::NoPassword | Self::PasswordMismatch => MatrixError { + errcode: "M_FORBIDDEN", + error: "Invalid username/password", + status: StatusCode::FORBIDDEN, + }, Self::LoginTookTooLong => MatrixError { errcode: "M_FORBIDDEN", error: "Login token expired", @@ -258,6 +266,11 @@ impl IntoResponse for RouteError { error: "Invalid login token", status: StatusCode::FORBIDDEN, }, + Self::UserLocked => MatrixError { + errcode: "M_USER_LOCKED", + error: "User account has been locked", + status: StatusCode::UNAUTHORIZED, + }, }; (sentry_event_id, response).into_response() @@ -398,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 @@ -501,7 +518,15 @@ async fn token_login( browser_session.id = %browser_session_id, "Attempt to exchange login token but browser session is not active" ); - return Err(RouteError::InvalidLoginToken); + return Err( + if browser_session.finished_at.is_some() + || browser_session.user.deactivated_at.is_some() + { + RouteError::InvalidLoginToken + } else { + RouteError::UserLocked + }, + ); } // We're about to create a device, let's explicitly acquire a lock, so that @@ -560,9 +585,13 @@ async fn user_password_login( .user() .find_by_username(username) .await? - .filter(mas_data_model::User::is_valid) + .filter(|user| user.deactivated_at.is_none()) .ok_or(RouteError::UserNotFound)?; + if user.locked_at.is_some() { + return Err(RouteError::UserLocked); + } + // Check the rate limit limiter.check_password(requester, &user)?; @@ -576,28 +605,32 @@ async fn user_password_login( // Verify the password let password = Zeroizing::new(password); - let new_password_hash = password_manager + match password_manager .verify_and_upgrade( &mut rng, user_password.version, password, user_password.hashed_password.clone(), ) - .await - .map_err(RouteError::PasswordVerificationFailed)?; - - if let Some((version, hashed_password)) = new_password_hash { - // Save the upgraded password if needed - repo.user_password() - .add( - &mut rng, - clock, - &user, - version, - hashed_password, - Some(&user_password), - ) - .await?; + .await? + { + PasswordVerificationResult::Success(Some((version, hashed_password))) => { + // Save the upgraded password if needed + repo.user_password() + .add( + &mut rng, + clock, + &user, + version, + hashed_password, + Some(&user_password), + ) + .await?; + } + PasswordVerificationResult::Success(None) => {} + PasswordVerificationResult::Failure => { + return Err(RouteError::PasswordMismatch); + } } // We're about to create a device, let's explicitly acquire a lock, so that @@ -776,7 +809,12 @@ mod tests { "###); } - async fn user_with_password(state: &TestState, username: &str, password: &str) { + async fn user_with_password( + state: &TestState, + username: &str, + password: &str, + locked: bool, + ) -> User { let mut rng = state.rng(); let mut repo = state.repository().await.unwrap(); @@ -795,14 +833,20 @@ 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(); + let user = if locked { + repo.user().lock(&state.clock, user).await.unwrap() + } else { + user + }; + repo.save().await.unwrap(); + user } /// Test that a user can login with a password using the Matrix @@ -812,7 +856,7 @@ mod tests { setup(); let state = TestState::from_pool(pool).await.unwrap(); - user_with_password(&state, "alice", "password").await; + let user = user_with_password(&state, "alice", "password", true).await; // Now let's try to login with the password, without asking for a refresh token. let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({ @@ -824,14 +868,30 @@ mod tests { "password": "password", })); + // First try to login to a locked account + let response = state.request(request.clone()).await; + response.assert_status(StatusCode::UNAUTHORIZED); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_USER_LOCKED", + "error": "User account has been locked" + } + "###); + + // Now try again after unlocking the account + let mut repo = state.repository().await.unwrap(); + let user = repo.user().unlock(user).await.unwrap(); + repo.save().await.unwrap(); + let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); insta::assert_json_snapshot!(body, @r###" { - "access_token": "mct_16tugBE5Ta9LIWoSJaAEHHq2g3fx8S_alcBB4", - "device_id": "ZGpSvYQqlq", + "access_token": "mct_cxG6gZXyvelQWW9XqfNbm5KAQovodf_XvJz43", + "device_id": "42oTpLoieH", "user_id": "@alice:example.com" } "###); @@ -853,10 +913,10 @@ mod tests { let body: serde_json::Value = response.json(); insta::assert_json_snapshot!(body, @r###" { - "access_token": "mct_cxG6gZXyvelQWW9XqfNbm5KAQovodf_XvJz43", - "device_id": "42oTpLoieH", + "access_token": "mct_PGMLvvMXC4Ds1A3lCWc6Hx4l9DGzqG_lVEIV2", + "device_id": "Yp7FM44zJN", "user_id": "@alice:example.com", - "refresh_token": "mcr_7IvDc44woP66fRQoS9MVcHXO9OeBmR_0jDGr1", + "refresh_token": "mcr_LoYqtrtBUBcWlE4RX6o47chBCGkadB_9gzpc1", "expires_in_ms": 300000 } "###); @@ -874,8 +934,8 @@ mod tests { let body: serde_json::Value = response.json(); insta::assert_json_snapshot!(body, @r###" { - "access_token": "mct_PGMLvvMXC4Ds1A3lCWc6Hx4l9DGzqG_lVEIV2", - "device_id": "Yp7FM44zJN", + "access_token": "mct_Xl3bbpfh9yNy9NzuRxyR3b3PLW0rqd_DiXAH2", + "device_id": "6cq7FqNSYo", "user_id": "@alice:example.com" } "###); @@ -921,6 +981,45 @@ mod tests { // The response should be the same as the previous one, so that we don't leak if // it's the user that is invalid or the password. assert_eq!(body, old_body); + + // Try to login to a deactivated account + let mut repo = state.repository().await.unwrap(); + let user = repo.user().deactivate(&state.clock, user).await.unwrap(); + repo.save().await.unwrap(); + + let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({ + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": "alice", + }, + "password": "password", + })); + + let response = state.request(request.clone()).await; + response.assert_status(StatusCode::FORBIDDEN); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_FORBIDDEN", + "error": "Invalid username/password" + } + "###); + + // Should get the same error if the deactivated user is also locked + let mut repo = state.repository().await.unwrap(); + let _user = repo.user().lock(&state.clock, user).await.unwrap(); + repo.save().await.unwrap(); + + let response = state.request(request).await; + response.assert_status(StatusCode::FORBIDDEN); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_FORBIDDEN", + "error": "Invalid username/password" + } + "###); } /// Test that we can send a login request without a Content-Type header @@ -929,7 +1028,7 @@ mod tests { setup(); let state = TestState::from_pool(pool).await.unwrap(); - user_with_password(&state, "alice", "password").await; + user_with_password(&state, "alice", "password", false).await; // Try without a Content-Type header let mut request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({ "type": "m.login.password", @@ -961,7 +1060,7 @@ mod tests { setup(); let state = TestState::from_pool(pool).await.unwrap(); - user_with_password(&state, "alice", "password").await; + let user = user_with_password(&state, "alice", "password", true).await; // Login with a full MXID as identifier let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({ @@ -973,13 +1072,29 @@ mod tests { "password": "password", })); + // First try to login to a locked account + let response = state.request(request.clone()).await; + response.assert_status(StatusCode::UNAUTHORIZED); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_USER_LOCKED", + "error": "User account has been locked" + } + "###); + + // Now try again after unlocking the account + let mut repo = state.repository().await.unwrap(); + let _ = repo.user().unlock(user).await.unwrap(); + repo.save().await.unwrap(); + let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); insta::assert_json_snapshot!(body, @r###" { - "access_token": "mct_16tugBE5Ta9LIWoSJaAEHHq2g3fx8S_alcBB4", - "device_id": "ZGpSvYQqlq", + "access_token": "mct_cxG6gZXyvelQWW9XqfNbm5KAQovodf_XvJz43", + "device_id": "42oTpLoieH", "user_id": "@alice:example.com" } "###); @@ -1021,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(); @@ -1123,12 +1237,13 @@ mod tests { .add(&mut state.rng(), &state.clock, "alice".to_owned()) .await .unwrap(); + // Start with a locked account + 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(); @@ -1155,14 +1270,29 @@ mod tests { "type": "m.login.token", "token": token, })); + let response = state.request(request.clone()).await; + response.assert_status(StatusCode::UNAUTHORIZED); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_USER_LOCKED", + "error": "User account has been locked" + } + "###); + + // Now try again after unlocking the account + let mut repo = state.repository().await.unwrap(); + let user = repo.user().unlock(user).await.unwrap(); + repo.save().await.unwrap(); + let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); insta::assert_json_snapshot!(body, @r#" { - "access_token": "mct_bnkWh1tPmm1MZOpygPaXwygX8PfxEY_hE6do1", - "device_id": "O3Ju1MUh3Z", + "access_token": "mct_bUTa4XIh92RARTPTjqQrCZLAkq2ild_0VsYE6", + "device_id": "uihy4bk51g", "user_id": "@alice:example.com" } "#); @@ -1203,6 +1333,41 @@ mod tests { "error": "Login token expired" } "###); + + // Try to login to a deactivated account + let token = get_login_token(&state, &user).await; + + let mut repo = state.repository().await.unwrap(); + let user = repo.user().deactivate(&state.clock, user).await.unwrap(); + repo.save().await.unwrap(); + let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({ + "type": "m.login.token", + "token": token, + })); + let response = state.request(request.clone()).await; + response.assert_status(StatusCode::FORBIDDEN); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_FORBIDDEN", + "error": "Invalid login token" + } + "###); + + // Should get the same error if the deactivated user is also locked + let mut repo = state.repository().await.unwrap(); + let _user = repo.user().lock(&state.clock, user).await.unwrap(); + repo.save().await.unwrap(); + + let response = state.request(request).await; + response.assert_status(StatusCode::FORBIDDEN); + let body: serde_json::Value = response.json(); + insta::assert_json_snapshot!(body, @r###" + { + "errcode": "M_FORBIDDEN", + "error": "Invalid login token" + } + "###); } /// Get a login token for a user. diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 15da5fb4d..2c3b612b4 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/handlers/src/compat/login_sso_redirect.rs b/crates/handlers/src/compat/login_sso_redirect.rs index 583f24e9b..e2d63728d 100644 --- a/crates/handlers/src/compat/login_sso_redirect.rs +++ b/crates/handlers/src/compat/login_sso_redirect.rs @@ -1,15 +1,15 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Query, State}, response::IntoResponse, }; use hyper::StatusCode; -use mas_axum_utils::record_error; +use mas_axum_utils::{GenericError, InternalError}; use mas_router::{CompatLoginSsoAction, CompatLoginSsoComplete, UrlBuilder}; use mas_storage::{BoxClock, BoxRepository, BoxRng, compat::CompatSsoLoginRepository}; use rand::distributions::{Alphanumeric, DistString}; @@ -43,12 +43,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let sentry_event_id = record_error!(self, Self::Internal(_)); - let status_code = match &self { - Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::MissingRedirectUrl | Self::InvalidRedirectUrl => StatusCode::BAD_REQUEST, - }; - (status_code, sentry_event_id, format!("{self}")).into_response() + match self { + Self::Internal(e) => InternalError::new(e).into_response(), + Self::MissingRedirectUrl | Self::InvalidRedirectUrl => { + GenericError::new(StatusCode::BAD_REQUEST, self).into_response() + } + } } } diff --git a/crates/handlers/src/compat/logout.rs b/crates/handlers/src/compat/logout.rs index 7b2ea7d52..27b67b768 100644 --- a/crates/handlers/src/compat/logout.rs +++ b/crates/handlers/src/compat/logout.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/handlers/src/compat/logout_all.rs b/crates/handlers/src/compat/logout_all.rs index 489521313..9cf14e428 100644 --- a/crates/handlers/src/compat/logout_all.rs +++ b/crates/handlers/src/compat/logout_all.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/handlers/src/compat/mod.rs b/crates/handlers/src/compat/mod.rs index abf02a28c..1c30d5e04 100644 --- a/crates/handlers/src/compat/mod.rs +++ b/crates/handlers/src/compat/mod.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ Json, diff --git a/crates/handlers/src/compat/refresh.rs b/crates/handlers/src/compat/refresh.rs index 7511e0e99..02e623eb4 100644 --- a/crates/handlers/src/compat/refresh.rs +++ b/crates/handlers/src/compat/refresh.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{Json, extract::State, response::IntoResponse}; use chrono::Duration; diff --git a/crates/handlers/src/graphql/mod.rs b/crates/handlers/src/graphql/mod.rs index cfedd69e9..9f51e8752 100644 --- a/crates/handlers/src/graphql/mod.rs +++ b/crates/handlers/src/graphql/mod.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::module_name_repetitions)] @@ -341,8 +341,7 @@ pub async fn post( let request = async_graphql::http::receive_body( content_type, - body.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(), + body.map_err(std::io::Error::other).into_async_read(), MultipartOptions::default(), ) .await? diff --git a/crates/handlers/src/graphql/model/browser_sessions.rs b/crates/handlers/src/graphql/model/browser_sessions.rs index 5e15644e2..925288067 100644 --- a/crates/handlers/src/graphql/model/browser_sessions.rs +++ b/crates/handlers/src/graphql/model/browser_sessions.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{ Context, Description, ID, Object, diff --git a/crates/handlers/src/graphql/model/compat_sessions.rs b/crates/handlers/src/graphql/model/compat_sessions.rs index 90adb61fe..fbbd4ab1f 100644 --- a/crates/handlers/src/graphql/model/compat_sessions.rs +++ b/crates/handlers/src/graphql/model/compat_sessions.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Description, Enum, ID, Object}; diff --git a/crates/handlers/src/graphql/model/cursor.rs b/crates/handlers/src/graphql/model/cursor.rs index 6dba9f4db..b7b498d0f 100644 --- a/crates/handlers/src/graphql/model/cursor.rs +++ b/crates/handlers/src/graphql/model/cursor.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::connection::OpaqueCursor; use serde::{Deserialize, Serialize}; diff --git a/crates/handlers/src/graphql/model/matrix.rs b/crates/handlers/src/graphql/model/matrix.rs index 930742285..7316c0d63 100644 --- a/crates/handlers/src/graphql/model/matrix.rs +++ b/crates/handlers/src/graphql/model/matrix.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::SimpleObject; use mas_matrix::HomeserverConnection; @@ -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/model/mod.rs b/crates/handlers/src/graphql/model/mod.rs index 5a3137edf..063a63fb0 100644 --- a/crates/handlers/src/graphql/model/mod.rs +++ b/crates/handlers/src/graphql/model/mod.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{Enum, Interface, Object, SimpleObject}; use chrono::{DateTime, Utc}; diff --git a/crates/handlers/src/graphql/model/node.rs b/crates/handlers/src/graphql/model/node.rs index aa61f4b62..e63d2b387 100644 --- a/crates/handlers/src/graphql/model/node.rs +++ b/crates/handlers/src/graphql/model/node.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{ID, Interface}; use serde::{Deserialize, Serialize}; diff --git a/crates/handlers/src/graphql/model/oauth.rs b/crates/handlers/src/graphql/model/oauth.rs index 9ec94c288..4f628e8bd 100644 --- a/crates/handlers/src/graphql/model/oauth.rs +++ b/crates/handlers/src/graphql/model/oauth.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Description, Enum, ID, Object}; diff --git a/crates/handlers/src/graphql/model/site_config.rs b/crates/handlers/src/graphql/model/site_config.rs index 3a958d495..d6966907e 100644 --- a/crates/handlers/src/graphql/model/site_config.rs +++ b/crates/handlers/src/graphql/model/site_config.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::str_to_string)] // ComplexObject macro uses &str.to_string() diff --git a/crates/handlers/src/graphql/model/upstream_oauth.rs b/crates/handlers/src/graphql/model/upstream_oauth.rs index a2061210b..faa30a755 100644 --- a/crates/handlers/src/graphql/model/upstream_oauth.rs +++ b/crates/handlers/src/graphql/model/upstream_oauth.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, ID, Object}; diff --git a/crates/handlers/src/graphql/model/users.rs b/crates/handlers/src/graphql/model/users.rs index fb54580a0..11522c6b4 100644 --- a/crates/handlers/src/graphql/model/users.rs +++ b/crates/handlers/src/graphql/model/users.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{ diff --git a/crates/handlers/src/graphql/model/viewer/anonymous.rs b/crates/handlers/src/graphql/model/viewer/anonymous.rs index e3b4e5273..56506d959 100644 --- a/crates/handlers/src/graphql/model/viewer/anonymous.rs +++ b/crates/handlers/src/graphql/model/viewer/anonymous.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{ID, Object}; diff --git a/crates/handlers/src/graphql/model/viewer/mod.rs b/crates/handlers/src/graphql/model/viewer/mod.rs index 4623f8a3e..4eec8c77b 100644 --- a/crates/handlers/src/graphql/model/viewer/mod.rs +++ b/crates/handlers/src/graphql/model/viewer/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::Union; diff --git a/crates/handlers/src/graphql/mutations/browser_session.rs b/crates/handlers/src/graphql/mutations/browser_session.rs index 688e17dcb..551775dbf 100644 --- a/crates/handlers/src/graphql/mutations/browser_session.rs +++ b/crates/handlers/src/graphql/mutations/browser_session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{Context, Enum, ID, InputObject, Object}; use mas_storage::RepositoryAccess; diff --git a/crates/handlers/src/graphql/mutations/compat_session.rs b/crates/handlers/src/graphql/mutations/compat_session.rs index 3930b5670..973d46f05 100644 --- a/crates/handlers/src/graphql/mutations/compat_session.rs +++ b/crates/handlers/src/graphql/mutations/compat_session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Enum, ID, InputObject, Object}; @@ -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 8788b493c..f88668e2f 100644 --- a/crates/handlers/src/graphql/mutations/matrix.rs +++ b/crates/handlers/src/graphql/mutations/matrix.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Description, Enum, ID, InputObject, Object}; @@ -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/mod.rs b/crates/handlers/src/graphql/mutations/mod.rs index 21fca3d6c..af6caab62 100644 --- a/crates/handlers/src/graphql/mutations/mod.rs +++ b/crates/handlers/src/graphql/mutations/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod browser_session; mod compat_session; diff --git a/crates/handlers/src/graphql/mutations/oauth2_session.rs b/crates/handlers/src/graphql/mutations/oauth2_session.rs index 1d0282014..55723efc5 100644 --- a/crates/handlers/src/graphql/mutations/oauth2_session.rs +++ b/crates/handlers/src/graphql/mutations/oauth2_session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Description, Enum, ID, InputObject, Object}; @@ -78,7 +78,7 @@ pub struct EndOAuth2SessionInput { /// The payload of the `endOauth2Session` mutation. pub enum EndOAuth2SessionPayload { NotFound, - Ended(mas_data_model::Session), + Ended(Box), } /// The status of the `endOauth2Session` mutation. @@ -104,7 +104,7 @@ impl EndOAuth2SessionPayload { /// Returns the ended session. async fn oauth2_session(&self) -> Option { match self { - Self::Ended(session) => Some(OAuth2Session(session.clone())), + Self::Ended(session) => Some(OAuth2Session(*session.clone())), Self::NotFound => None, } } @@ -126,7 +126,7 @@ pub enum SetOAuth2SessionNamePayload { NotFound, /// The session was updated. - Updated(mas_data_model::Session), + Updated(Box), } /// The status of the `setOauth2SessionName` mutation. @@ -152,7 +152,7 @@ impl SetOAuth2SessionNamePayload { /// The session that was updated. async fn oauth2_session(&self) -> Option { match self { - Self::Updated(session) => Some(OAuth2Session(session.clone())), + Self::Updated(session) => Some(OAuth2Session(*session.clone())), Self::NotFound => None, } } @@ -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")?; } @@ -293,7 +292,7 @@ impl OAuth2SessionMutations { repo.save().await?; - Ok(EndOAuth2SessionPayload::Ended(session)) + Ok(EndOAuth2SessionPayload::Ended(Box::new(session))) } async fn set_oauth2_session_name( @@ -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")?; } @@ -343,6 +341,6 @@ impl OAuth2SessionMutations { repo.save().await?; - Ok(SetOAuth2SessionNamePayload::Updated(session)) + Ok(SetOAuth2SessionNamePayload::Updated(Box::new(session))) } } diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index f4ae549b9..f9f5696e7 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Description, Enum, ID, InputObject, Object}; @@ -563,7 +563,7 @@ impl UserMutations { Ok(LockUserPayload::Locked(user)) } - /// Unlock a user. This is only available to administrators. + /// Unlock and reactivate a user. This is only available to administrators. async fn unlock_user( &self, ctx: &Context<'_>, @@ -585,11 +585,11 @@ impl UserMutations { return Ok(UnlockUserPayload::NotFound); }; - // Call the homeserver synchronously to unlock the user - let mxid = matrix.mxid(&user.username); - matrix.reactivate_user(&mxid).await?; + // Call the homeserver synchronously to reactivate the user + matrix.reactivate_user(&user.username).await?; - // Now unlock the user in our database + // Now reactivate & unlock the user in our database + let user = repo.user().reactivate(user).await?; let user = repo.user().unlock(user).await?; repo.save().await?; @@ -653,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/mutations/user_email.rs b/crates/handlers/src/graphql/mutations/user_email.rs index 6f24f1ed4..63b825566 100644 --- a/crates/handlers/src/graphql/mutations/user_email.rs +++ b/crates/handlers/src/graphql/mutations/user_email.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use async_graphql::{Context, Description, Enum, ID, InputObject, Object}; diff --git a/crates/handlers/src/graphql/query/mod.rs b/crates/handlers/src/graphql/query/mod.rs index eb86150e5..66e6b38bb 100644 --- a/crates/handlers/src/graphql/query/mod.rs +++ b/crates/handlers/src/graphql/query/mod.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{Context, ID, MergedObject, Object}; diff --git a/crates/handlers/src/graphql/query/session.rs b/crates/handlers/src/graphql/query/session.rs index 24368e5f7..921009ee9 100644 --- a/crates/handlers/src/graphql/query/session.rs +++ b/crates/handlers/src/graphql/query/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{Context, ID, Object, Union}; use mas_data_model::Device; diff --git a/crates/handlers/src/graphql/query/upstream_oauth.rs b/crates/handlers/src/graphql/query/upstream_oauth.rs index b007425aa..f52c21f82 100644 --- a/crates/handlers/src/graphql/query/upstream_oauth.rs +++ b/crates/handlers/src/graphql/query/upstream_oauth.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{ Context, ID, Object, diff --git a/crates/handlers/src/graphql/query/user.rs b/crates/handlers/src/graphql/query/user.rs index 9a2eeef45..364319e57 100644 --- a/crates/handlers/src/graphql/query/user.rs +++ b/crates/handlers/src/graphql/query/user.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{ Context, Enum, ID, Object, diff --git a/crates/handlers/src/graphql/query/viewer.rs b/crates/handlers/src/graphql/query/viewer.rs index 6985dfd2e..defcb5571 100644 --- a/crates/handlers/src/graphql/query/viewer.rs +++ b/crates/handlers/src/graphql/query/viewer.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{Context, Object}; diff --git a/crates/handlers/src/graphql/state.rs b/crates/handlers/src/graphql/state.rs index 737f43340..600fd232e 100644 --- a/crates/handlers/src/graphql/state.rs +++ b/crates/handlers/src/graphql/state.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_graphql::{Response, ServerError}; use mas_data_model::SiteConfig; diff --git a/crates/handlers/src/graphql/tests.rs b/crates/handlers/src/graphql/tests.rs index 08567314f..bc5079924 100644 --- a/crates/handlers/src/graphql/tests.rs +++ b/crates/handlers/src/graphql/tests.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::http::Request; use hyper::StatusCode; @@ -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/health.rs b/crates/handlers/src/health.rs index b1dfbf993..916df60df 100644 --- a/crates/handlers/src/health.rs +++ b/crates/handlers/src/health.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{extract::State, response::IntoResponse}; use mas_axum_utils::InternalError; diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 0605d6cd6..08367bbf5 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(clippy::future_not_send)] #![allow( @@ -257,7 +257,7 @@ where } #[allow(clippy::trait_duplication_in_bounds)] -pub fn compat_router() -> Router +pub fn compat_router(templates: Templates) -> Router where S: Clone + Send + Sync + 'static, UrlBuilder: FromRef, @@ -272,7 +272,28 @@ where BoxClock: FromRequestParts, BoxRng: FromRequestParts, { - Router::new() + // A sub-router for human-facing routes with error handling + let human_router = Router::new() + .route( + mas_router::CompatLoginSsoRedirect::route(), + get(self::compat::login_sso_redirect::get), + ) + .route( + mas_router::CompatLoginSsoRedirectIdp::route(), + get(self::compat::login_sso_redirect::get), + ) + .route( + mas_router::CompatLoginSsoRedirectSlash::route(), + get(self::compat::login_sso_redirect::get), + ) + .layer(AndThenLayer::new( + async move |response: axum::response::Response| { + Ok::<_, Infallible>(recover_error(&templates, response)) + }, + )); + + // A sub-router for API-facing routes with CORS + let api_router = Router::new() .route( mas_router::CompatLogin::route(), get(self::compat::login::get).post(self::compat::login::post), @@ -289,18 +310,6 @@ where mas_router::CompatRefresh::route(), post(self::compat::refresh::post), ) - .route( - mas_router::CompatLoginSsoRedirect::route(), - get(self::compat::login_sso_redirect::get), - ) - .route( - mas_router::CompatLoginSsoRedirectIdp::route(), - get(self::compat::login_sso_redirect::get), - ) - .route( - mas_router::CompatLoginSsoRedirectSlash::route(), - get(self::compat::login_sso_redirect::get), - ) .layer( CorsLayer::new() .allow_origin(Any) @@ -314,10 +323,11 @@ where HeaderName::from_static("x-requested-with"), ]) .max_age(Duration::from_secs(60 * 60)), - ) + ); + + Router::new().merge(human_router).merge(api_router) } -#[allow(clippy::too_many_lines)] pub fn human_router(templates: Templates) -> Router where S: Clone + Send + Sync + 'static, @@ -440,6 +450,10 @@ where mas_router::UpstreamOAuth2Link::route(), get(self::upstream_oauth2::link::get).post(self::upstream_oauth2::link::post), ) + .route( + mas_router::UpstreamOAuth2BackchannelLogout::route(), + post(self::upstream_oauth2::backchannel_logout::post), + ) .route( mas_router::DeviceCodeLink::route(), get(self::oauth2::device::link::get), @@ -450,22 +464,29 @@ where ) .layer(AndThenLayer::new( async move |response: axum::response::Response| { - // Error responses should have an ErrorContext attached to them - let ext = response.extensions().get::(); - if let Some(ctx) = ext { - if let Ok(res) = templates.render_error(ctx) { - let (mut parts, _original_body) = response.into_parts(); - parts.headers.remove(CONTENT_TYPE); - parts.headers.remove(CONTENT_LENGTH); - return Ok((parts, Html(res)).into_response()); - } - } - - Ok::<_, Infallible>(response) + Ok::<_, Infallible>(recover_error(&templates, response)) }, )) } +fn recover_error( + templates: &Templates, + response: axum::response::Response, +) -> axum::response::Response { + // Error responses should have an ErrorContext attached to them + let ext = response.extensions().get::(); + if let Some(ctx) = ext { + if let Ok(res) = templates.render_error(ctx) { + let (mut parts, _original_body) = response.into_parts(); + parts.headers.remove(CONTENT_TYPE); + parts.headers.remove(CONTENT_LENGTH); + return (parts, Html(res)).into_response(); + } + } + + response +} + /// The fallback handler for all routes that don't match anything else. /// /// # Errors diff --git a/crates/handlers/src/oauth2/authorization/callback.rs b/crates/handlers/src/oauth2/authorization/callback.rs index beb0868d7..01f59d602 100644 --- a/crates/handlers/src/oauth2/authorization/callback.rs +++ b/crates/handlers/src/oauth2/authorization/callback.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::module_name_repetitions)] diff --git a/crates/handlers/src/oauth2/authorization/consent.rs b/crates/handlers/src/oauth2/authorization/consent.rs index 14dfd0e7f..4a10b96a3 100644 --- a/crates/handlers/src/oauth2/authorization/consent.rs +++ b/crates/handlers/src/oauth2/authorization/consent.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Form, Path, State}, @@ -11,9 +11,9 @@ use axum::{ use axum_extra::TypedHeader; use hyper::StatusCode; use mas_axum_utils::{ + GenericError, InternalError, cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, - record_error, }; use mas_data_model::AuthorizationGrantStage; use mas_keystore::Keystore; @@ -64,13 +64,15 @@ impl_from_error_for_route!(super::callback::CallbackDestinationError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let sentry_event_id = record_error!(self, Self::Internal(_) | Self::NoSuchClient(_)); - ( - StatusCode::INTERNAL_SERVER_ERROR, - sentry_event_id, - self.to_string(), - ) - .into_response() + match self { + Self::Internal(e) => InternalError::new(e).into_response(), + e @ Self::NoSuchClient(_) => InternalError::new(Box::new(e)).into_response(), + e @ Self::GrantNotFound => GenericError::new(StatusCode::NOT_FOUND, e).into_response(), + e @ Self::GrantNotPending(_) => { + GenericError::new(StatusCode::CONFLICT, e).into_response() + } + e @ Self::Csrf(_) => GenericError::new(StatusCode::BAD_REQUEST, e).into_response(), + } } } diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index c3b080eae..6dfaf4020 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -1,15 +1,15 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Form, State}, response::{IntoResponse, Response}, }; use hyper::StatusCode; -use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, record_error}; +use mas_axum_utils::{GenericError, InternalError, SessionInfoExt, cookies::CookieJar}; use mas_data_model::{AuthorizationCode, Pkce}; use mas_router::{PostAuthAction, UrlBuilder}; use mas_storage::{ @@ -53,29 +53,15 @@ pub enum RouteError { impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let sentry_event_id = record_error!(self, Self::Internal(_)); - // TODO: better error pages - let response = match self { - RouteError::Internal(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + match self { + Self::Internal(e) => InternalError::new(e).into_response(), + e @ (Self::ClientNotFound + | Self::InvalidResponseMode + | Self::IntoCallbackDestination(_) + | Self::UnknownRedirectUri(_)) => { + GenericError::new(StatusCode::BAD_REQUEST, e).into_response() } - RouteError::ClientNotFound => { - (StatusCode::BAD_REQUEST, "could not find client").into_response() - } - RouteError::InvalidResponseMode => { - (StatusCode::BAD_REQUEST, "invalid response mode").into_response() - } - RouteError::IntoCallbackDestination(e) => { - (StatusCode::BAD_REQUEST, e.to_string()).into_response() - } - RouteError::UnknownRedirectUri(e) => ( - StatusCode::BAD_REQUEST, - format!("Invalid redirect URI ({e})"), - ) - .into_response(), - }; - - (sentry_event_id, response).into_response() + } } } @@ -123,7 +109,6 @@ fn resolve_response_mode( fields(client.id = %params.auth.client_id), skip_all, )] -#[allow(clippy::too_many_lines)] pub(crate) async fn get( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/oauth2/device/authorize.rs b/crates/handlers/src/oauth2/device/authorize.rs index 1feec8c3e..b4651f4b8 100644 --- a/crates/handlers/src/oauth2/device/authorize.rs +++ b/crates/handlers/src/oauth2/device/authorize.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{Json, extract::State, response::IntoResponse}; use axum_extra::typed_header::TypedHeader; diff --git a/crates/handlers/src/oauth2/device/consent.rs b/crates/handlers/src/oauth2/device/consent.rs index 05e1d502d..c8114c3a0 100644 --- a/crates/handlers/src/oauth2/device/consent.rs +++ b/crates/handlers/src/oauth2/device/consent.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context; use axum::{ diff --git a/crates/handlers/src/oauth2/device/link.rs b/crates/handlers/src/oauth2/device/link.rs index 0e3c8bd2c..538aed3c6 100644 --- a/crates/handlers/src/oauth2/device/link.rs +++ b/crates/handlers/src/oauth2/device/link.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Query, State}, diff --git a/crates/handlers/src/oauth2/device/mod.rs b/crates/handlers/src/oauth2/device/mod.rs index d874cf19b..565ce5df5 100644 --- a/crates/handlers/src/oauth2/device/mod.rs +++ b/crates/handlers/src/oauth2/device/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub mod authorize; pub mod consent; diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs index bdefa3e62..61dfc1ba5 100644 --- a/crates/handlers/src/oauth2/discovery.rs +++ b/crates/handlers/src/oauth2/discovery.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{Json, extract::State, response::IntoResponse}; use mas_iana::oauth::{ @@ -35,7 +35,6 @@ struct DiscoveryResponse { } #[tracing::instrument(name = "handlers.oauth2.discovery.get", skip_all)] -#[allow(clippy::too_many_lines)] pub(crate) async fn get( State(key_store): State, State(url_builder): State, diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 5c1e00a36..678c7fa1c 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -1,10 +1,13 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::BTreeSet, sync::LazyLock}; +use std::{ + collections::BTreeSet, + sync::{Arc, LazyLock}, +}; use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse}; use hyper::{HeaderMap, StatusCode}; @@ -15,6 +18,7 @@ use mas_axum_utils::{ use mas_data_model::{Device, TokenFormatError, TokenType}; use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint}; use mas_keystore::Encrypter; +use mas_matrix::HomeserverConnection; use mas_storage::{ BoxClock, BoxRepository, Clock, compat::{CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository}, @@ -102,8 +106,14 @@ pub enum RouteError { #[error("bad request")] BadRequest, + #[error("failed to verify token")] + FailedToVerifyToken(#[source] anyhow::Error), + #[error(transparent)] ClientCredentialsVerification(#[from] CredentialsVerificationError), + + #[error("bearer token presented is invalid")] + InvalidBearerToken, } impl IntoResponse for RouteError { @@ -114,13 +124,15 @@ impl IntoResponse for RouteError { | Self::CantLoadCompatSession(_) | Self::CantLoadOAuthSession(_) | Self::CantLoadUser(_) + | Self::FailedToVerifyToken(_) ); let response = match self { e @ (Self::Internal(_) | Self::CantLoadCompatSession(_) | Self::CantLoadOAuthSession(_) - | Self::CantLoadUser(_)) => ( + | Self::CantLoadUser(_) + | Self::FailedToVerifyToken(_)) => ( StatusCode::INTERNAL_SERVER_ERROR, Json( ClientError::from(ClientErrorCode::ServerError).with_description(e.to_string()), @@ -140,6 +152,14 @@ impl IntoResponse for RouteError { ), ) .into_response(), + e @ Self::InvalidBearerToken => ( + StatusCode::UNAUTHORIZED, + Json( + ClientError::from(ClientErrorCode::AccessDenied) + .with_description(e.to_string()), + ), + ) + .into_response(), Self::UnknownToken(_) | Self::UnexpectedTokenType @@ -219,38 +239,50 @@ fn normalize_scope(mut scope: Scope) -> Scope { #[tracing::instrument( name = "handlers.oauth2.introspection.post", - fields(client.id = client_authorization.client_id()), + fields(client.id = credentials.client_id()), skip_all, )] -#[allow(clippy::too_many_lines)] pub(crate) async fn post( clock: BoxClock, State(http_client): State, mut repo: BoxRepository, activity_tracker: ActivityTracker, State(encrypter): State, + State(homeserver): State>, headers: HeaderMap, - client_authorization: ClientAuthorization, + ClientAuthorization { credentials, form }: ClientAuthorization, ) -> Result { - let client = client_authorization - .credentials - .fetch(&mut repo) - .await? - .ok_or(RouteError::ClientNotFound)?; - - let method = match &client.token_endpoint_auth_method { - None | Some(OAuthClientAuthenticationMethod::None) => { - return Err(RouteError::NotAllowed(client.id)); + if let Some(token) = credentials.bearer_token() { + // If the client presented a bearer token, we check with the homeserver + // configuration if it is allowed to use the introspection endpoint + if !homeserver + .verify_token(token) + .await + .map_err(RouteError::FailedToVerifyToken)? + { + return Err(RouteError::InvalidBearerToken); } - Some(c) => c, - }; + } else { + // Otherwise, it presented regular client credentials, so we verify them + let client = credentials + .fetch(&mut repo) + .await? + .ok_or(RouteError::ClientNotFound)?; - client_authorization - .credentials - .verify(&http_client, &encrypter, method, &client) - .await?; + // Only confidential clients are allowed to introspect + let method = match &client.token_endpoint_auth_method { + None | Some(OAuthClientAuthenticationMethod::None) => { + return Err(RouteError::NotAllowed(client.id)); + } + Some(c) => c, + }; - let Some(form) = client_authorization.form else { + credentials + .verify(&http_client, &encrypter, method, &client) + .await?; + } + + let Some(form) = form else { return Err(RouteError::BadRequest); }; @@ -606,10 +638,11 @@ mod tests { use hyper::{Request, StatusCode}; use mas_data_model::{AccessToken, RefreshToken}; use mas_iana::oauth::OAuthTokenTypeHint; - use mas_matrix::{HomeserverConnection, ProvisionRequest}; + use mas_matrix::{HomeserverConnection, MockHomeserverConnection, ProvisionRequest}; use mas_router::{OAuth2Introspection, OAuth2RegistrationEndpoint, SimpleRoute}; use mas_storage::Clock; use oauth2_types::{ + errors::{ClientError, ClientErrorCode}, registration::ClientRegistrationResponse, requests::IntrospectionResponse, scope::{OPENID, Scope}, @@ -662,10 +695,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(); @@ -863,10 +895,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(); @@ -1014,4 +1045,29 @@ mod tests { let response: IntrospectionResponse = response.json(); assert!(response.active); } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_introspect_with_bearer_token(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + // Check that talking to the introspection endpoint with the bearer token from + // the MockHomeserverConnection doens't error out + let request = Request::post(OAuth2Introspection::PATH) + .bearer(MockHomeserverConnection::VALID_BEARER_TOKEN) + .form(json!({ "token": "some_token" })); + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: IntrospectionResponse = response.json(); + assert!(!response.active); + + // Check with another token, we should get a 401 + let request = Request::post(OAuth2Introspection::PATH) + .bearer("another_token") + .form(json!({ "token": "some_token" })); + let response = state.request(request).await; + response.assert_status(StatusCode::UNAUTHORIZED); + let response: ClientError = response.json(); + assert_eq!(response.error, ClientErrorCode::AccessDenied); + } } diff --git a/crates/handlers/src/oauth2/keys.rs b/crates/handlers/src/oauth2/keys.rs index bcb419969..04e4135e3 100644 --- a/crates/handlers/src/oauth2/keys.rs +++ b/crates/handlers/src/oauth2/keys.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{Json, extract::State, response::IntoResponse}; use mas_keystore::Keystore; diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index f15a1ae9d..cbe81072f 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index f3f91d754..09ace7351 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/handlers/src/oauth2/revoke.rs b/crates/handlers/src/oauth2/revoke.rs index 758f0d647..4889257b4 100644 --- a/crates/handlers/src/oauth2/revoke.rs +++ b/crates/handlers/src/oauth2/revoke.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 3c8c9db20..4b4c8dca6 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock}; @@ -409,7 +409,6 @@ pub(crate) async fn post( Ok((headers, Json(reply))) } -#[allow(clippy::too_many_lines)] // TODO: refactor some parts out async fn authorization_code_grant( mut rng: &mut BoxRng, clock: &impl Clock, @@ -575,11 +574,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)?; } @@ -599,7 +601,6 @@ async fn authorization_code_grant( Ok((params, repo)) } -#[allow(clippy::too_many_lines)] async fn refresh_token_grant( rng: &mut BoxRng, clock: &impl Clock, @@ -951,11 +952,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/handlers/src/oauth2/userinfo.rs b/crates/handlers/src/oauth2/userinfo.rs index 064196e3d..338e98ec7 100644 --- a/crates/handlers/src/oauth2/userinfo.rs +++ b/crates/handlers/src/oauth2/userinfo.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ Json, diff --git a/crates/handlers/src/oauth2/webfinger.rs b/crates/handlers/src/oauth2/webfinger.rs index 5c4b3c96b..8289e495c 100644 --- a/crates/handlers/src/oauth2/webfinger.rs +++ b/crates/handlers/src/oauth2/webfinger.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ Json, diff --git a/crates/handlers/src/passwords.rs b/crates/handlers/src/passwords.rs index f4ce10adb..6f32f77f9 100644 --- a/crates/handlers/src/passwords.rs +++ b/crates/handlers/src/passwords.rs @@ -1,15 +1,15 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, sync::Arc}; use anyhow::Context; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString}; use futures_util::future::OptionFuture; -use pbkdf2::Pbkdf2; +use pbkdf2::{Pbkdf2, password_hash}; use rand::{CryptoRng, RngCore, SeedableRng, distributions::Standard, prelude::Distribution}; use thiserror::Error; use zeroize::Zeroizing; @@ -17,6 +17,50 @@ use zxcvbn::zxcvbn; pub type SchemeVersion = u16; +/// The result of a password verification, which is `true` if the password +/// matches the hashed password, and `false` otherwise. +/// +/// In the success case it can also contain additional data, such as the new +/// hashing scheme and the new hashed password. +#[must_use] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PasswordVerificationResult { + /// The password matches the stored password hash + Success(T), + /// The password does not match the stored password hash + Failure, +} + +impl PasswordVerificationResult<()> { + fn success() -> Self { + Self::Success(()) + } + + fn failure() -> Self { + Self::Failure + } +} + +impl PasswordVerificationResult { + /// Converts the result into a new result with the given data. + fn with_data(self, data: N) -> PasswordVerificationResult { + match self { + Self::Success(_) => PasswordVerificationResult::Success(data), + Self::Failure => PasswordVerificationResult::Failure, + } + } +} + +impl From for PasswordVerificationResult<()> { + fn from(value: bool) -> Self { + if value { + Self::success() + } else { + Self::failure() + } + } +} + #[derive(Debug, Error)] #[error("Password manager is disabled")] pub struct PasswordManagerDisabledError; @@ -149,11 +193,11 @@ impl PasswordManager { scheme: SchemeVersion, password: Zeroizing, hashed_password: String, - ) -> Result<(), anyhow::Error> { + ) -> Result { let inner = self.get_inner()?; let span = tracing::Span::current(); - tokio::task::spawn_blocking(move || { + let result = tokio::task::spawn_blocking(move || { span.in_scope(move || { let hasher = if scheme == inner.current_version { &inner.current_hasher @@ -169,7 +213,7 @@ impl PasswordManager { }) .await??; - Ok(()) + Ok(result) } /// Verify a password hash for the given hashing scheme, and upgrade it on @@ -186,7 +230,7 @@ impl PasswordManager { scheme: SchemeVersion, password: Zeroizing, hashed_password: String, - ) -> Result, anyhow::Error> { + ) -> Result>, anyhow::Error> { let inner = self.get_inner()?; // If the current scheme isn't the default one, we also hash with the default @@ -198,11 +242,11 @@ impl PasswordManager { let verify_fut = self.verify(scheme, password, hashed_password); let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut); - verify_res?; + let password_result = verify_res?; let new_hash = new_hash_res.transpose()?; - Ok(new_hash) + Ok(password_result.with_data(new_hash)) } } @@ -276,7 +320,7 @@ impl Hasher { &self, hashed_password: &str, password: Zeroizing, - ) -> Result<(), anyhow::Error> { + ) -> Result { let password = self.normalize_password(password); self.algorithm @@ -345,8 +389,8 @@ impl Algorithm { hashed_password: &str, password: &[u8], pepper: Option<&[u8]>, - ) -> Result<(), anyhow::Error> { - match self { + ) -> Result { + let result = match self { Algorithm::Bcrypt { .. } => { let mut password = Zeroizing::new(password.to_vec()); if let Some(pepper) = pepper { @@ -354,7 +398,7 @@ impl Algorithm { } let result = bcrypt::verify(password, hashed_password)?; - anyhow::ensure!(result, "wrong password"); + PasswordVerificationResult::from(result) } Algorithm::Argon2id => { @@ -370,7 +414,11 @@ impl Algorithm { let hashed_password = PasswordHash::new(hashed_password)?; - phf.verify_password(password.as_ref(), &hashed_password)?; + match phf.verify_password(password.as_ref(), &hashed_password) { + Ok(()) => PasswordVerificationResult::success(), + Err(password_hash::Error::Password) => PasswordVerificationResult::failure(), + Err(e) => Err(e)?, + } } Algorithm::Pbkdf2 => { @@ -381,11 +429,15 @@ impl Algorithm { let hashed_password = PasswordHash::new(hashed_password)?; - Pbkdf2.verify_password(password.as_ref(), &hashed_password)?; + match Pbkdf2.verify_password(password.as_ref(), &hashed_password) { + Ok(()) => PasswordVerificationResult::success(), + Err(password_hash::Error::Password) => PasswordVerificationResult::failure(), + Err(e) => Err(e)?, + } } - } + }; - Ok(()) + Ok(result) } } @@ -410,10 +462,26 @@ mod tests { .expect("Couldn't hash password"); insta::assert_snapshot!(hash); - assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok()); - assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err()); - assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err()); - assert!(alg.verify_blocking(&hash, password, None).is_err()); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Success(()) + ); + assert_eq!( + alg.verify_blocking(&hash, password2, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper2)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, None) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); // Hash without pepper let hash = alg @@ -421,9 +489,21 @@ mod tests { .expect("Couldn't hash password"); insta::assert_snapshot!(hash); - assert!(alg.verify_blocking(&hash, password, None).is_ok()); - assert!(alg.verify_blocking(&hash, password2, None).is_err()); - assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err()); + assert_eq!( + alg.verify_blocking(&hash, password, None) + .expect("Verification failed"), + PasswordVerificationResult::Success(()) + ); + assert_eq!( + alg.verify_blocking(&hash, password2, None) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); } #[test] @@ -441,10 +521,26 @@ mod tests { .expect("Couldn't hash password"); insta::assert_snapshot!(hash); - assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok()); - assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err()); - assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err()); - assert!(alg.verify_blocking(&hash, password, None).is_err()); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Success(()) + ); + assert_eq!( + alg.verify_blocking(&hash, password2, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper2)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, None) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); // Hash without pepper let hash = alg @@ -452,9 +548,21 @@ mod tests { .expect("Couldn't hash password"); insta::assert_snapshot!(hash); - assert!(alg.verify_blocking(&hash, password, None).is_ok()); - assert!(alg.verify_blocking(&hash, password2, None).is_err()); - assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err()); + assert_eq!( + alg.verify_blocking(&hash, password, None) + .expect("Verification failed"), + PasswordVerificationResult::Success(()) + ); + assert_eq!( + alg.verify_blocking(&hash, password2, None) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); } #[test] @@ -473,10 +581,26 @@ mod tests { .expect("Couldn't hash password"); insta::assert_snapshot!(hash); - assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok()); - assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err()); - assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err()); - assert!(alg.verify_blocking(&hash, password, None).is_err()); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Success(()) + ); + assert_eq!( + alg.verify_blocking(&hash, password2, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper2)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, None) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); // Hash without pepper let hash = alg @@ -484,12 +608,23 @@ mod tests { .expect("Couldn't hash password"); insta::assert_snapshot!(hash); - assert!(alg.verify_blocking(&hash, password, None).is_ok()); - assert!(alg.verify_blocking(&hash, password2, None).is_err()); - assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err()); + assert_eq!( + alg.verify_blocking(&hash, password, None) + .expect("Verification failed"), + PasswordVerificationResult::Success(()) + ); + assert_eq!( + alg.verify_blocking(&hash, password2, None) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); + assert_eq!( + alg.verify_blocking(&hash, password, Some(pepper)) + .expect("Verification failed"), + PasswordVerificationResult::Failure + ); } - #[allow(clippy::too_many_lines)] #[tokio::test] async fn hash_verify_and_upgrade() { // Tests the whole password manager, by hashing a password and upgrading it @@ -520,16 +655,18 @@ mod tests { insta::assert_snapshot!(hash); // Just verifying works - manager + let res = manager .verify(version, password.clone(), hash.clone()) .await .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Success(())); // And doesn't work with the wrong password - manager + let res = manager .verify(version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); // Verifying with the wrong version doesn't work manager @@ -543,13 +680,14 @@ mod tests { .await .expect("Failed to verify"); - assert!(res.is_none()); + assert_eq!(res, PasswordVerificationResult::Success(None)); // Upgrading still verify that the password matches - manager + let res = manager .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); let manager = PasswordManager::new( 0, @@ -564,16 +702,18 @@ mod tests { .unwrap(); // Verifying still works - manager + let res = manager .verify(version, password.clone(), hash.clone()) .await .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Success(())); // And doesn't work with the wrong password - manager + let res = manager .verify(version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); // Upgrading does re-hash let res = manager @@ -581,9 +721,9 @@ mod tests { .await .expect("Failed to verify"); - assert!(res.is_some()); - let (version, hash) = res.unwrap(); - + let PasswordVerificationResult::Success(Some((version, hash))) = res else { + panic!("Expected a successful upgrade"); + }; assert_eq!(version, 2); insta::assert_snapshot!(hash); @@ -593,19 +733,21 @@ mod tests { .await .expect("Failed to verify"); - assert!(res.is_none()); + assert_eq!(res, PasswordVerificationResult::Success(None)); // Upgrading still verify that the password matches - manager + let res = manager .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); // Upgrading still verify that the password matches - manager + let res = manager .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); let manager = PasswordManager::new( 0, @@ -624,16 +766,18 @@ mod tests { .unwrap(); // Verifying still works - manager + let res = manager .verify(version, password.clone(), hash.clone()) .await .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Success(())); // And doesn't work with the wrong password - manager + let res = manager .verify(version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); // Upgrading does re-hash let res = manager @@ -641,8 +785,9 @@ mod tests { .await .expect("Failed to verify"); - assert!(res.is_some()); - let (version, hash) = res.unwrap(); + let PasswordVerificationResult::Success(Some((version, hash))) = res else { + panic!("Expected a successful upgrade"); + }; assert_eq!(version, 3); insta::assert_snapshot!(hash); @@ -653,12 +798,13 @@ mod tests { .await .expect("Failed to verify"); - assert!(res.is_none()); + assert_eq!(res, PasswordVerificationResult::Success(None)); // Upgrading still verify that the password matches - manager + let res = manager .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone()) .await - .expect_err("Verification should have failed"); + .expect("Failed to verify"); + assert_eq!(res, PasswordVerificationResult::Failure); } } diff --git a/crates/handlers/src/preferred_language.rs b/crates/handlers/src/preferred_language.rs index cfa0e106d..8ea38c990 100644 --- a/crates/handlers/src/preferred_language.rs +++ b/crates/handlers/src/preferred_language.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{convert::Infallible, sync::Arc}; diff --git a/crates/handlers/src/rate_limit.rs b/crates/handlers/src/rate_limit.rs index bb5642036..1714d4fed 100644 --- a/crates/handlers/src/rate_limit.rs +++ b/crates/handlers/src/rate_limit.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{net::IpAddr, sync::Arc, time::Duration}; diff --git a/crates/handlers/src/session.rs b/crates/handlers/src/session.rs index 9eac19307..75638f5cd 100644 --- a/crates/handlers/src/session.rs +++ b/crates/handlers/src/session.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Utilities for showing proposer HTML fallbacks when the user is logged out, //! locked or deactivated diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index f5a7403da..7927c1431 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ convert::Infallible, @@ -29,6 +29,7 @@ use mas_axum_utils::{ }; use mas_config::RateLimitingConfig; use mas_data_model::SiteConfig; +use mas_email::{MailTransport, Mailer}; use mas_i18n::Translator; use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey}; use mas_matrix::{HomeserverConnection, MockHomeserverConnection}; @@ -39,6 +40,7 @@ use mas_storage::{ clock::MockClock, }; use mas_storage_pg::PgRepositoryFactory; +use mas_tasks::QueueWorker; use mas_templates::{SiteConfigExt, Templates}; use oauth2_types::{registration::ClientRegistrationResponse, requests::AccessTokenResponse}; use rand::SeedableRng; @@ -113,6 +115,7 @@ pub(crate) struct TestState { pub rng: Arc>, pub http_client: reqwest::Client, pub task_tracker: TaskTracker, + queue_worker: Arc>, #[allow(dead_code)] // It is used, as it will cancel the CancellationToken when dropped cancellation_drop_guard: Arc, @@ -235,6 +238,27 @@ impl TestState { shutdown_token.child_token(), ); + let mailer = Mailer::new( + templates.clone(), + MailTransport::blackhole(), + "hello@example.com".parse().unwrap(), + "hello@example.com".parse().unwrap(), + ); + + let queue_worker = mas_tasks::init( + PgRepositoryFactory::new(pool.clone()), + Arc::clone(&clock), + &mailer, + homeserver_connection.clone(), + url_builder.clone(), + &site_config, + shutdown_token.child_token(), + ) + .await + .unwrap(); + + let queue_worker = Arc::new(tokio::sync::Mutex::new(queue_worker)); + Ok(Self { repository_factory: PgRepositoryFactory::new(pool), templates, @@ -254,10 +278,19 @@ impl TestState { rng, http_client, task_tracker, + queue_worker, cancellation_drop_guard: Arc::new(shutdown_token.drop_guard()), }) } + /// Run all the available jobs in the queue. + /// + /// Panics if it fails to run the jobs (but not on job failures!) + pub async fn run_jobs_in_queue(&self) { + let mut queue = self.queue_worker.lock().await; + queue.process_all_jobs_in_tests().await.unwrap(); + } + /// Reset the test utils to a fresh state, with the same configuration. pub async fn reset(self) -> Self { let site_config = self.site_config.clone(); @@ -286,7 +319,7 @@ impl TestState { let app = crate::healthcheck_router() .merge(crate::discovery_router()) .merge(crate::api_router()) - .merge(crate::compat_router()) + .merge(crate::compat_router(self.templates.clone())) .merge(crate::human_router(self.templates.clone())) // We enable undocumented_oauth2_access for the tests, as it is easier to query the API // with it diff --git a/crates/handlers/src/upstream_oauth2/authorize.rs b/crates/handlers/src/upstream_oauth2/authorize.rs index 43403c137..016dd36d3 100644 --- a/crates/handlers/src/upstream_oauth2/authorize.rs +++ b/crates/handlers/src/upstream_oauth2/authorize.rs @@ -1,15 +1,15 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Path, Query, State}, response::{IntoResponse, Redirect}, }; use hyper::StatusCode; -use mas_axum_utils::{cookies::CookieJar, record_error}; +use mas_axum_utils::{GenericError, InternalError, cookies::CookieJar}; use mas_data_model::UpstreamOAuthProvider; use mas_oidc_client::requests::authorization_code::AuthorizationRequestData; use mas_router::{PostAuthAction, UrlBuilder}; @@ -41,13 +41,12 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let sentry_event_id = record_error!(self, Self::Internal(_)); - let response = match self { - Self::ProviderNotFound => (StatusCode::NOT_FOUND, "Provider not found").into_response(), - Self::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), - }; - - (sentry_event_id, response).into_response() + match self { + e @ Self::ProviderNotFound => { + GenericError::new(StatusCode::NOT_FOUND, e).into_response() + } + Self::Internal(e) => InternalError::new(e).into_response(), + } } } diff --git a/crates/handlers/src/upstream_oauth2/backchannel_logout.rs b/crates/handlers/src/upstream_oauth2/backchannel_logout.rs new file mode 100644 index 000000000..9e2a034b9 --- /dev/null +++ b/crates/handlers/src/upstream_oauth2/backchannel_logout.rs @@ -0,0 +1,316 @@ +// 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::{HashMap, HashSet}; + +use axum::{ + Form, Json, + extract::{Path, State, rejection::FormRejection}, + response::IntoResponse, +}; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_data_model::{UpstreamOAuthProvider, UpstreamOAuthProviderOnBackchannelLogout}; +use mas_jose::{ + claims::{self, Claim, TimeOptions}, + jwt::JwtDecodeError, +}; +use mas_oidc_client::{ + error::JwtVerificationError, + requests::jose::{JwtVerificationData, verify_signed_jwt}, +}; +use mas_storage::{ + BoxClock, BoxRepository, BoxRng, Pagination, + compat::CompatSessionFilter, + oauth2::OAuth2SessionFilter, + queue::{QueueJobRepositoryExt as _, SyncDevicesJob}, + upstream_oauth2::UpstreamOAuthSessionFilter, + user::BrowserSessionFilter, +}; +use oauth2_types::errors::{ClientError, ClientErrorCode}; +use serde::Deserialize; +use serde_json::Value; +use thiserror::Error; +use ulid::Ulid; + +use crate::{MetadataCache, impl_from_error_for_route, upstream_oauth2::cache::LazyProviderInfos}; + +#[derive(Debug, Error)] +pub enum RouteError { + /// An internal error occurred. + #[error(transparent)] + Internal(Box), + + /// Invalid request body + #[error(transparent)] + InvalidRequestBody(#[from] FormRejection), + + /// Logout token is not a JWT + #[error("failed to decode logout token")] + InvalidLogoutToken(#[from] JwtDecodeError), + + /// Logout token failed to be verified + #[error("failed to verify logout token")] + LogoutTokenVerification(#[from] JwtVerificationError), + + /// Logout token had invalid claims + #[error("invalid claims in logout token")] + InvalidLogoutTokenClaims(#[from] claims::ClaimError), + + /// Logout token has neither a sub nor a sid claim + #[error("logout token has neither a sub nor a sid claim")] + NoSubOrSidClaim, + + /// Provider not found + #[error("provider not found")] + ProviderNotFound, +} + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let sentry_event_id = record_error!(self, Self::Internal(_)); + + let response = match self { + e @ Self::Internal(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json( + ClientError::from(ClientErrorCode::ServerError).with_description(e.to_string()), + ), + ) + .into_response(), + + e @ (Self::InvalidLogoutToken(_) + | Self::LogoutTokenVerification(_) + | Self::InvalidRequestBody(_) + | Self::InvalidLogoutTokenClaims(_) + | Self::NoSubOrSidClaim) => ( + StatusCode::BAD_REQUEST, + Json( + ClientError::from(ClientErrorCode::InvalidRequest) + .with_description(e.to_string()), + ), + ) + .into_response(), + + Self::ProviderNotFound => ( + StatusCode::NOT_FOUND, + Json( + ClientError::from(ClientErrorCode::InvalidRequest).with_description( + "Upstream OAuth provider not found, is the backchannel logout URI right?" + .to_owned(), + ), + ), + ) + .into_response(), + }; + + (sentry_event_id, response).into_response() + } +} + +impl_from_error_for_route!(mas_storage::RepositoryError); +impl_from_error_for_route!(mas_oidc_client::error::DiscoveryError); +impl_from_error_for_route!(mas_oidc_client::error::JwksError); + +#[derive(Deserialize)] +pub(crate) struct BackchannelLogoutRequest { + logout_token: String, +} + +#[derive(Deserialize)] +struct LogoutTokenEvents { + #[allow(dead_code)] // We just want to check it deserializes + #[serde(rename = "http://schemas.openid.net/event/backchannel-logout")] + backchannel_logout: HashMap, +} + +const EVENTS: Claim = Claim::new("events"); + +#[tracing::instrument( + name = "handlers.upstream_oauth2.backchannel_logout.post", + fields(upstream_oauth_provider.id = %provider_id), + skip_all, +)] +pub(crate) async fn post( + clock: BoxClock, + mut rng: BoxRng, + mut repo: BoxRepository, + State(metadata_cache): State, + State(client): State, + Path(provider_id): Path, + request: Result, FormRejection>, +) -> Result { + let Form(request) = request?; + let provider = repo + .upstream_oauth_provider() + .lookup(provider_id) + .await? + .filter(UpstreamOAuthProvider::enabled) + .ok_or(RouteError::ProviderNotFound)?; + + let mut lazy_metadata = LazyProviderInfos::new(&metadata_cache, &provider, &client); + + let jwks = + mas_oidc_client::requests::jose::fetch_jwks(&client, lazy_metadata.jwks_uri().await?) + .await?; + + // Validate the logout token. The rules are defined in + // + // + // Upon receiving a logout request at the back-channel logout URI, the RP MUST + // validate the Logout Token as follows: + // + // 1. If the Logout Token is encrypted, decrypt it using the keys and + // algorithms that the Client specified during Registration that the OP was + // to use to encrypt ID Tokens. If ID Token encryption was negotiated with + // the OP at Registration time and the Logout Token is not encrypted, the RP + // SHOULD reject it. + // 2. Validate the Logout Token signature in the same way that an ID Token + // signature is validated, with the following refinements. + // 3. Validate the alg (algorithm) Header Parameter in the same way it is + // validated for ID Tokens. Like ID Tokens, selection of the algorithm used + // is governed by the id_token_signing_alg_values_supported Discovery + // parameter and the id_token_signed_response_alg Registration parameter + // when they are used; otherwise, the value SHOULD be the default of RS256. + // Additionally, an alg with the value none MUST NOT be used for Logout + // Tokens. + // 4. Validate the iss, aud, iat, and exp Claims in the same way they are + // validated in ID Tokens. + // 5. Verify that the Logout Token contains a sub Claim, a sid Claim, or both. + // 6. Verify that the Logout Token contains an events Claim whose value is JSON + // object containing the member name http://schemas.openid.net/event/backchannel-logout. + // 7. Verify that the Logout Token does not contain a nonce Claim. + // 8. Optionally verify that another Logout Token with the same jti value has + // not been recently received. + // 9. Optionally verify that the iss Logout Token Claim matches the iss Claim + // in an ID Token issued for the current session or a recent session of this + // RP with the OP. + // 10. Optionally verify that any sub Logout Token Claim matches the sub Claim + // in an ID Token issued for the current session or a recent session of + // this RP with the OP. + // 11. Optionally verify that any sid Logout Token Claim matches the sid Claim + // in an ID Token issued for the current session or a recent session of + // this RP with the OP. + // + // If any of the validation steps fails, reject the Logout Token and return an + // HTTP 400 Bad Request error. Otherwise, proceed to perform the logout actions. + // + // The ISS and AUD claims are already checked by the verify_signed_jwt() + // function. + + // This verifies (1), (2), (3) and the iss and aud claims for (4) + let token = verify_signed_jwt( + &request.logout_token, + JwtVerificationData { + issuer: provider.issuer.as_deref(), + jwks: &jwks, + client_id: &provider.client_id, + signing_algorithm: &provider.id_token_signed_response_alg, + }, + )?; + + let (_header, mut claims) = token.into_parts(); + + let time_options = TimeOptions::new(clock.now()); + claims::EXP.extract_required_with_options(&mut claims, &time_options)?; // (4) + claims::IAT.extract_required_with_options(&mut claims, &time_options)?; // (4) + + let sub = claims::SUB.extract_optional(&mut claims)?; // (5) + let sid = claims::SID.extract_optional(&mut claims)?; // (5) + if sub.is_none() && sid.is_none() { + return Err(RouteError::NoSubOrSidClaim); + } + + EVENTS.extract_required(&mut claims)?; // (6) + claims::NONCE.assert_absent(&claims)?; // (7) + + // Find the corresponding upstream OAuth 2.0 sessions + let mut auth_session_filter = UpstreamOAuthSessionFilter::new().for_provider(&provider); + if let Some(sub) = &sub { + auth_session_filter = auth_session_filter.with_sub_claim(sub); + } + if let Some(sid) = &sid { + auth_session_filter = auth_session_filter.with_sid_claim(sid); + } + let count = repo + .upstream_oauth_session() + .count(auth_session_filter) + .await?; + + tracing::info!(sub, sid, %provider.id, "Backchannel logout received, found {count} corresponding authentication sessions"); + + match provider.on_backchannel_logout { + UpstreamOAuthProviderOnBackchannelLogout::DoNothing => { + tracing::warn!(%provider.id, "Provider configured to do nothing on backchannel logout"); + } + UpstreamOAuthProviderOnBackchannelLogout::LogoutBrowserOnly => { + let filter = BrowserSessionFilter::new() + .authenticated_by_upstream_sessions_only(auth_session_filter) + .active_only(); + let affected = repo.browser_session().finish_bulk(&clock, filter).await?; + tracing::info!("Finished {affected} browser sessions"); + } + UpstreamOAuthProviderOnBackchannelLogout::LogoutAll => { + let browser_session_filter = BrowserSessionFilter::new() + .authenticated_by_upstream_sessions_only(auth_session_filter); + + // We need to loop through all the browser sessions to find all the + // users affected so that we can trigger a device sync job for them + let mut cursor = Pagination::first(1000); + let mut user_ids = HashSet::new(); + loop { + let browser_sessions = repo + .browser_session() + .list(browser_session_filter, cursor) + .await?; + for browser_session in browser_sessions.edges { + user_ids.insert(browser_session.user.id); + cursor = cursor.after(browser_session.id); + } + + if !browser_sessions.has_next_page { + break; + } + } + + let browser_sessions_affected = repo + .browser_session() + .finish_bulk(&clock, browser_session_filter.active_only()) + .await?; + + let oauth2_session_filter = OAuth2SessionFilter::new() + .active_only() + .for_browser_sessions(browser_session_filter); + + let oauth2_sessions_affected = repo + .oauth2_session() + .finish_bulk(&clock, oauth2_session_filter) + .await?; + + let compat_session_filter = CompatSessionFilter::new() + .active_only() + .for_browser_sessions(browser_session_filter); + + let compat_sessions_affected = repo + .compat_session() + .finish_bulk(&clock, compat_session_filter) + .await?; + + tracing::info!( + "Finished {browser_sessions_affected} browser sessions, {oauth2_sessions_affected} OAuth 2.0 sessions and {compat_sessions_affected} compatibility sessions" + ); + + for user_id in user_ids { + tracing::info!(user.id = %user_id, "Queueing a device sync job for user"); + let job = SyncDevicesJob::new_for_id(user_id); + repo.queue_job().schedule_job(&mut rng, &clock, job).await?; + } + } + } + + repo.save().await?; + + Ok(()) +} diff --git a/crates/handlers/src/upstream_oauth2/cache.rs b/crates/handlers/src/upstream_oauth2/cache.rs index 6c1b7de63..cfee7b78e 100644 --- a/crates/handlers/src/upstream_oauth2/cache.rs +++ b/crates/handlers/src/upstream_oauth2/cache.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, sync::Arc}; @@ -165,6 +165,10 @@ impl MetadataCache { /// /// This spawns a background task that will refresh the cache at the given /// interval. + /// + /// # Errors + /// + /// Returns an error if the warm up task could not be started. #[tracing::instrument(name = "metadata_cache.warm_up_and_run", skip_all)] pub async fn warm_up_and_run( &self, @@ -237,6 +241,10 @@ impl MetadataCache { } /// Get the metadata for the given issuer. + /// + /// # Errors + /// + /// Returns an error if the metadata could not be retrieved. #[tracing::instrument(name = "metadata_cache.get", fields(%issuer), skip_all)] pub async fn get( &self, @@ -290,13 +298,12 @@ impl MetadataCache { #[cfg(test)] mod tests { - #![allow(clippy::too_many_lines)] - // XXX: sadly, we can't test HTTPS requests with wiremock, so we can only test // 'insecure' discovery use mas_data_model::{ - UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod, + UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderOnBackchannelLogout, + UpstreamOAuthProviderTokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use mas_storage::{Clock, clock::MockClock}; @@ -427,6 +434,7 @@ mod tests { claims_imports: UpstreamOAuthProviderClaimsImports::default(), additional_authorization_parameters: Vec::new(), forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }; // Without any override, it should just use discovery diff --git a/crates/handlers/src/upstream_oauth2/callback.rs b/crates/handlers/src/upstream_oauth2/callback.rs index 75e8d63a0..de1c087f7 100644 --- a/crates/handlers/src/upstream_oauth2/callback.rs +++ b/crates/handlers/src/upstream_oauth2/callback.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; @@ -13,7 +13,7 @@ use axum::{ response::{Html, IntoResponse, Response}, }; use hyper::StatusCode; -use mas_axum_utils::{cookies::CookieJar, record_error}; +use mas_axum_utils::{GenericError, InternalError, cookies::CookieJar}; use mas_data_model::{UpstreamOAuthProvider, UpstreamOAuthProviderResponseMode}; use mas_jose::claims::TokenHash; use mas_keystore::{Encrypter, Keystore}; @@ -153,15 +153,13 @@ impl_from_error_for_route!(super::cookie::UpstreamSessionNotFound); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { - let sentry_event_id = record_error!(self, Self::Internal(_)); - let response = match self { - Self::ProviderNotFound => (StatusCode::NOT_FOUND, "Provider not found").into_response(), - Self::SessionNotFound => (StatusCode::NOT_FOUND, "Session not found").into_response(), - Self::Internal(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), - e => (StatusCode::BAD_REQUEST, e.to_string()).into_response(), - }; - - (sentry_event_id, response).into_response() + match self { + Self::Internal(e) => InternalError::new(e).into_response(), + e @ (Self::ProviderNotFound | Self::SessionNotFound) => { + GenericError::new(StatusCode::NOT_FOUND, e).into_response() + } + e => GenericError::new(StatusCode::BAD_REQUEST, e).into_response(), + } } } @@ -170,7 +168,7 @@ impl IntoResponse for RouteError { fields(upstream_oauth_provider.id = %provider_id), skip_all, )] -#[allow(clippy::too_many_lines, clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments)] pub(crate) async fn handler( mut rng: BoxRng, clock: BoxClock, @@ -312,6 +310,7 @@ pub(crate) async fn handler( .await?; let mut jwks = None; + let mut id_token_claims = None; let mut context = AttributeMappingContext::new(); if let Some(id_token) = token_response.id_token.as_ref() { @@ -337,6 +336,14 @@ pub(crate) async fn handler( let (_headers, mut claims) = id_token.into_parts(); + // Save a copy of the claims for later; the claims extract methods + // remove them from the map, and we want to store the original claims. + // We anyway need this to be a serde_json::Value + id_token_claims = Some( + serde_json::to_value(&claims) + .expect("serializing a HashMap into a Value should never fail"), + ); + // Access token hash must match. mas_jose::claims::AT_HASH .extract_optional_with_options( @@ -472,6 +479,7 @@ pub(crate) async fn handler( session, &link, token_response.id_token, + id_token_claims, params.extra_callback_parameters, userinfo, ) diff --git a/crates/handlers/src/upstream_oauth2/cookie.rs b/crates/handlers/src/upstream_oauth2/cookie.rs index cbcfb5148..b978ddaa9 100644 --- a/crates/handlers/src/upstream_oauth2/cookie.rs +++ b/crates/handlers/src/upstream_oauth2/cookie.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // TODO: move that to a standalone cookie manager diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index d95854faa..0182f0538 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock}; @@ -19,6 +19,7 @@ use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, record_error, }; +use mas_data_model::UpstreamOAuthProviderOnConflict; use mas_jose::jwt::Jwt; use mas_matrix::HomeserverConnection; use mas_policy::Policy; @@ -37,7 +38,6 @@ use minijinja::Environment; use opentelemetry::{Key, KeyValue, metrics::Counter}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tracing::warn; use ulid::Ulid; use super::{ @@ -420,8 +420,10 @@ pub(crate) async fn get( &context, provider.claims_imports.displayname.is_required(), )? { - Some(value) => ctx - .with_display_name(value, provider.claims_imports.displayname.is_forced()), + Some(value) => ctx.with_display_name( + value, + provider.claims_imports.displayname.is_forced_or_required(), + ), None => ctx, } }; @@ -442,7 +444,9 @@ pub(crate) async fn get( &context, provider.claims_imports.email.is_required(), )? { - Some(value) => ctx.with_email(value, provider.claims_imports.email.is_forced()), + Some(value) => { + ctx.with_email(value, provider.claims_imports.email.is_forced_or_required()) + } None => ctx, } }; @@ -473,19 +477,49 @@ pub(crate) async fn get( .await .map_err(RouteError::HomeserverConnection)?; - if maybe_existing_user.is_some() || !is_available { - if let Some(existing_user) = maybe_existing_user { - // The mapper returned a username which already exists, but isn't - // linked to this upstream user. - warn!(username = %localpart, user_id = %existing_user.id, "Localpart template returned an existing username"); - } + if let Some(existing_user) = maybe_existing_user { + // The mapper returned a username which already exists, but isn't + // linked to this upstream user. + let on_conflict = provider.claims_imports.localpart.on_conflict; + match on_conflict { + UpstreamOAuthProviderOnConflict::Fail => { + // TODO: translate + let ctx = ErrorContext::new() + .with_code("User exists") + .with_description(format!( + r"Upstream account provider returned {localpart:?} as username, + which is not linked to that upstream account. Your homeserver does not allow + linking an upstream account to an existing account" + )) + .with_language(&locale); + + return Ok(( + cookie_jar, + Html(templates.render_error(&ctx)?).into_response(), + )); + } + UpstreamOAuthProviderOnConflict::Add => { + // new oauth link is allowed + let ctx = UpstreamExistingLinkContext::new(existing_user) + .with_csrf(csrf_token.form_value()) + .with_language(locale); + + return Ok(( + cookie_jar, + Html(templates.render_upstream_oauth2_login_link(&ctx)?) + .into_response(), + )); + } + } + } + + if !is_available { // TODO: translate let ctx = ErrorContext::new() - .with_code("User exists") + .with_code("Localpart not available") .with_description(format!( - r"Upstream account provider returned {localpart:?} as username, - which is not linked to that upstream account" + r"Localpart {localpart:?} is not available on this homeserver" )) .with_language(&locale); @@ -511,9 +545,9 @@ pub(crate) async fn get( // The username passes the policy check, add it to the context ctx.with_localpart( localpart, - provider.claims_imports.localpart.is_forced(), + provider.claims_imports.localpart.is_forced_or_required(), ) - } else if provider.claims_imports.localpart.is_forced() { + } else if provider.claims_imports.localpart.is_forced_or_required() { // If the username claim is 'forced' but doesn't pass the policy check, // we display an error message. // TODO: translate @@ -618,6 +652,80 @@ pub(crate) async fn post( session } + (None, None, FormData::Link) => { + // There is an existing user with the same username, but no link. + // If the configuration allows it, the user is prompted to link the + // existing account. Note that we cannot trust the user input here, + // which is why we have to re-calculate the localpart, instead of + // passing it through form data. + + let id_token = upstream_session.id_token().map(Jwt::try_from).transpose()?; + + let provider = repo + .upstream_oauth_provider() + .lookup(link.provider_id) + .await? + .ok_or(RouteError::ProviderNotFound(link.provider_id))?; + + let env = environment(); + + let mut context = AttributeMappingContext::new(); + if let Some(id_token) = id_token { + let (_, payload) = id_token.into_parts(); + context = context.with_id_token_claims(payload); + } + if let Some(extra_callback_parameters) = upstream_session.extra_callback_parameters() { + context = context.with_extra_callback_parameters(extra_callback_parameters.clone()); + } + if let Some(userinfo) = upstream_session.userinfo() { + context = context.with_userinfo_claims(userinfo.clone()); + } + let context = context.build(); + + if !provider.claims_imports.localpart.is_forced_or_required() { + //Claims import for `localpart` should be `require` or `force` at this stage + return Err(RouteError::InvalidFormAction); + } + + let template = provider + .claims_imports + .localpart + .template + .as_deref() + .unwrap_or(DEFAULT_LOCALPART_TEMPLATE); + + let Some(localpart) = render_attribute_template(&env, template, &context, true)? else { + // This should never be the case at this point + return Err(RouteError::InvalidFormAction); + }; + + let maybe_user = repo.user().find_by_username(&localpart).await?; + + let Some(user) = maybe_user else { + // user cannot be None at this stage + return Err(RouteError::InvalidFormAction); + }; + + let on_conflict = provider.claims_imports.localpart.on_conflict; + + match on_conflict { + UpstreamOAuthProviderOnConflict::Fail => { + //OnConflict can not be equals to Fail at this stage + return Err(RouteError::InvalidFormAction); + } + UpstreamOAuthProviderOnConflict::Add => { + //add link to the user + repo.upstream_oauth_link() + .associate_to_user(&link, &user) + .await?; + + repo.browser_session() + .add(&mut rng, &clock, &user, user_agent) + .await? + } + } + } + ( None, None, @@ -690,7 +798,7 @@ pub(crate) async fn post( let ctx = if let Some(ref display_name) = display_name { ctx.with_display_name( display_name.clone(), - provider.claims_imports.email.is_forced(), + provider.claims_imports.email.is_forced_or_required(), ) } else { ctx @@ -715,12 +823,15 @@ pub(crate) async fn post( }; let ctx = if let Some(ref email) = email { - ctx.with_email(email.clone(), provider.claims_imports.email.is_forced()) + ctx.with_email( + email.clone(), + provider.claims_imports.email.is_forced_or_required(), + ) } else { ctx }; - let username = if provider.claims_imports.localpart.is_forced() { + let username = if provider.claims_imports.localpart.is_forced_or_required() { let template = provider .claims_imports .localpart @@ -737,7 +848,7 @@ pub(crate) async fn post( let ctx = ctx.with_localpart( username.clone(), - provider.claims_imports.localpart.is_forced(), + provider.claims_imports.localpart.is_forced_or_required(), ); // Validate the form @@ -900,16 +1011,21 @@ pub(crate) async fn post( mod tests { use hyper::{Request, StatusCode, header::CONTENT_TYPE}; use mas_data_model::{ - UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderImportPreference, + UpstreamOAuthAuthorizationSession, UpstreamOAuthLink, UpstreamOAuthProviderClaimsImports, + UpstreamOAuthProviderImportPreference, UpstreamOAuthProviderLocalpartPreference, UpstreamOAuthProviderTokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use mas_jose::jwt::{JsonWebSignatureHeader, Jwt}; + use mas_keystore::Keystore; use mas_router::Route; use mas_storage::{ - Pagination, upstream_oauth2::UpstreamOAuthProviderParams, user::UserEmailFilter, + Pagination, Repository, RepositoryError, upstream_oauth2::UpstreamOAuthProviderParams, + user::UserEmailFilter, }; use oauth2_types::scope::{OPENID, Scope}; + use rand_chacha::ChaChaRng; + use serde_json::Value; use sqlx::PgPool; use super::UpstreamSessionsCookie; @@ -923,9 +1039,10 @@ mod tests { let cookies = CookieHelper::new(); let claims_imports = UpstreamOAuthProviderClaimsImports { - localpart: UpstreamOAuthProviderImportPreference { + localpart: UpstreamOAuthProviderLocalpartPreference { action: mas_data_model::UpstreamOAuthProviderImportAction::Force, template: None, + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::default(), }, email: UpstreamOAuthProviderImportPreference { action: mas_data_model::UpstreamOAuthProviderImportAction::Force, @@ -934,7 +1051,7 @@ mod tests { ..UpstreamOAuthProviderClaimsImports::default() }; - let id_token = serde_json::json!({ + let id_token_claims = serde_json::json!({ "preferred_username": "john", "email": "john@example.com", "email_verified": true, @@ -953,7 +1070,8 @@ mod tests { .signing_key_for_alg(&JsonWebSignatureAlg::Rs256) .unwrap(); let header = JsonWebSignatureHeader::new(JsonWebSignatureAlg::Rs256); - let id_token = Jwt::sign_with_rng(&mut rng, header, id_token, &signer).unwrap(); + let id_token = + Jwt::sign_with_rng(&mut rng, header, id_token_claims.clone(), &signer).unwrap(); // Provision a provider and a link let mut repo = state.repository().await.unwrap(); @@ -985,6 +1103,8 @@ mod tests { additional_authorization_parameters: Vec::new(), forward_login_hint: false, ui_order: 0, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, ) .await @@ -1022,6 +1142,7 @@ mod tests { session, &link, Some(id_token.into_string()), + Some(id_token_claims), None, None, ) @@ -1095,4 +1216,310 @@ mod tests { assert_eq!(email.email, "john@example.com"); } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_link_existing_account(pool: PgPool) { + let existing_username = "john"; + let subject = "subject"; + + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + let mut rng = state.rng(); + let cookies = CookieHelper::new(); + + let claims_imports = UpstreamOAuthProviderClaimsImports { + localpart: UpstreamOAuthProviderLocalpartPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::Add, + }, + email: UpstreamOAuthProviderImportPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + }, + ..UpstreamOAuthProviderClaimsImports::default() + }; + + //`preferred_username` matches an existing user's username + let id_token_claims = serde_json::json!({ + "preferred_username": existing_username, + "email": "any@example.com", + "email_verified": true, + }); + + let id_token = sign_token(&mut rng, &state.key_store, id_token_claims.clone()).unwrap(); + + // Provision a provider and a link + let mut repo = state.repository().await.unwrap(); + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &state.clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }, + ) + .await + .unwrap(); + + //provision upstream authorization session to setup cookies + let (link, session) = add_linked_upstream_session( + &mut rng, + &state.clock, + &mut repo, + &provider, + subject, + &id_token.into_string(), + id_token_claims, + ) + .await + .unwrap(); + + let cookie_jar = state.cookie_jar(); + let upstream_sessions = UpstreamSessionsCookie::default() + .add(session.id, provider.id, "state".to_owned(), None) + .add_link_to_session(session.id, link.id) + .unwrap(); + let cookie_jar = upstream_sessions.save(cookie_jar, &state.clock); + cookies.import(cookie_jar); + + let user = repo + .user() + .add(&mut rng, &state.clock, existing_username.to_owned()) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let request = Request::get(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + let request = Request::post(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).form( + serde_json::json!({ + "csrf": csrf_token, + "action": "link" + }), + ); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + + // Check that the existing user has the oidc link + let mut repo = state.repository().await.unwrap(); + + let link = repo + .upstream_oauth_link() + .find_by_subject(&provider, subject) + .await + .unwrap() + .expect("link exists"); + + assert_eq!(link.user_id, Some(user.id)); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_link_existing_account_when_not_allowed_by_default(pool: PgPool) { + let existing_username = "john"; + + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + let mut rng = state.rng(); + let cookies = CookieHelper::new(); + + let claims_imports = UpstreamOAuthProviderClaimsImports { + localpart: UpstreamOAuthProviderLocalpartPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + on_conflict: mas_data_model::UpstreamOAuthProviderOnConflict::default(), + }, + email: UpstreamOAuthProviderImportPreference { + action: mas_data_model::UpstreamOAuthProviderImportAction::Require, + template: None, + }, + ..UpstreamOAuthProviderClaimsImports::default() + }; + + // `preferred_username` matches an existing user's username + let id_token_claims = serde_json::json!({ + "preferred_username": existing_username, + "email": "any@example.com", + "email_verified": true, + }); + + let id_token = sign_token(&mut rng, &state.key_store, id_token_claims.clone()).unwrap(); + + // Provision a provider and a link + let mut repo = state.repository().await.unwrap(); + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &state.clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }, + ) + .await + .unwrap(); + + let (link, session) = add_linked_upstream_session( + &mut rng, + &state.clock, + &mut repo, + &provider, + "subject", + &id_token.into_string(), + id_token_claims, + ) + .await + .unwrap(); + + // Provision an user + repo.user() + .add(&mut rng, &state.clock, existing_username.to_owned()) + .await + .unwrap(); + + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let upstream_sessions = UpstreamSessionsCookie::default() + .add(session.id, provider.id, "state".to_owned(), None) + .add_link_to_session(session.id, link.id) + .unwrap(); + let cookie_jar = upstream_sessions.save(cookie_jar, &state.clock); + cookies.import(cookie_jar); + + let request = Request::get(&*mas_router::UpstreamOAuth2Link::new(link.id).path()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + assert!(response.body().contains("Unexpected error")); + } + + fn sign_token( + rng: &mut ChaChaRng, + keystore: &Keystore, + payload: Value, + ) -> Result, mas_jose::jwt::JwtSignatureError> { + let key = keystore + .signing_key_for_algorithm(&JsonWebSignatureAlg::Rs256) + .unwrap(); + + let signer = key + .params() + .signing_key_for_alg(&JsonWebSignatureAlg::Rs256) + .unwrap(); + + let header = JsonWebSignatureHeader::new(JsonWebSignatureAlg::Rs256); + + Jwt::sign_with_rng(rng, header, payload, &signer) + } + + async fn add_linked_upstream_session( + rng: &mut ChaChaRng, + clock: &impl mas_storage::Clock, + repo: &mut Box + Send + Sync + 'static>, + provider: &mas_data_model::UpstreamOAuthProvider, + subject: &str, + id_token: &str, + id_token_claims: Value, + ) -> Result<(UpstreamOAuthLink, UpstreamOAuthAuthorizationSession), anyhow::Error> { + let session = repo + .upstream_oauth_session() + .add( + rng, + clock, + provider, + "state".to_owned(), + None, + Some("nonce".to_owned()), + ) + .await?; + + let link = repo + .upstream_oauth_link() + .add(rng, clock, provider, subject.to_owned(), None) + .await?; + + let session = repo + .upstream_oauth_session() + .complete_with_link( + clock, + session, + &link, + Some(id_token.to_owned()), + Some(id_token_claims), + None, + None, + ) + .await?; + + Ok((link, session)) + } } diff --git a/crates/handlers/src/upstream_oauth2/mod.rs b/crates/handlers/src/upstream_oauth2/mod.rs index c387aca1b..272af648b 100644 --- a/crates/handlers/src/upstream_oauth2/mod.rs +++ b/crates/handlers/src/upstream_oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::string::FromUtf8Error; @@ -16,6 +16,7 @@ use thiserror::Error; use url::Url; pub(crate) mod authorize; +pub(crate) mod backchannel_logout; pub(crate) mod cache; pub(crate) mod callback; mod cookie; diff --git a/crates/handlers/src/upstream_oauth2/template.rs b/crates/handlers/src/upstream_oauth2/template.rs index cdd193f09..fcf24473a 100644 --- a/crates/handlers/src/upstream_oauth2/template.rs +++ b/crates/handlers/src/upstream_oauth2/template.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, sync::Arc}; diff --git a/crates/handlers/src/views/app.rs b/crates/handlers/src/views/app.rs index 47b657436..b0bb3e31c 100644 --- a/crates/handlers/src/views/app.rs +++ b/crates/handlers/src/views/app.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Query, State}, diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index c05f4e307..bc6b19f37 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::State, diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 1db44285a..a89e98612 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock}; @@ -38,7 +38,7 @@ use zeroize::Zeroizing; use super::shared::OptionalPostAuthAction; use crate::{ BoundActivityTracker, Limiter, METER, PreferredLanguage, RequesterFingerprint, SiteConfig, - passwords::PasswordManager, + passwords::{PasswordManager, PasswordVerificationResult}, session::{SessionOrFallback, load_session_or_fallback}, }; @@ -166,6 +166,7 @@ pub(crate) async fn post( } if !form_state.is_valid() { + tracing::warn!("Invalid login form: {form_state:?}"); PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( locale, @@ -189,6 +190,7 @@ pub(crate) async fn post( // First, lookup the user let Some(user) = get_user_by_email_or_by_username(site_config, &mut repo, username).await? else { + tracing::warn!(username, "User not found"); let form_state = form_state.with_error_on_form(FormError::InvalidCredentials); PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( @@ -207,7 +209,7 @@ pub(crate) async fn post( // Check the rate limit if let Err(e) = limiter.check_password(requester, &user) { - tracing::warn!(error = &e as &dyn std::error::Error); + tracing::warn!(error = &e as &dyn std::error::Error, "ratelimit exceeded"); let form_state = form_state.with_error_on_form(FormError::RateLimitExceeded); PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( @@ -228,6 +230,7 @@ pub(crate) async fn post( let Some(user_password) = repo.user_password().active(&user).await? else { // There is no password for this user, but we don't want to disclose that. Show // a generic 'invalid credentials' error instead + tracing::warn!(username, "No password for user"); let form_state = form_state.with_error_on_form(FormError::InvalidCredentials); PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( @@ -256,7 +259,7 @@ pub(crate) async fn post( ) .await { - Ok(Some((version, new_password_hash))) => { + Ok(PasswordVerificationResult::Success(Some((version, new_password_hash)))) => { // Save the upgraded password repo.user_password() .add( @@ -269,10 +272,11 @@ pub(crate) async fn post( ) .await? } - Ok(None) => user_password, - Err(_) => { + Ok(PasswordVerificationResult::Success(None)) => user_password, + Ok(PasswordVerificationResult::Failure) => { + tracing::warn!(username, "Failed to verify/upgrade password for user"); let form_state = form_state.with_error_on_form(FormError::InvalidCredentials); - PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "mismatch")]); return render( locale, cookie_jar, @@ -286,11 +290,13 @@ pub(crate) async fn post( ) .await; } + Err(err) => return Err(InternalError::from_anyhow(err)), }; // Now that we have checked the user password, we now want to show an error if // the user is locked or deactivated if user.deactivated_at.is_some() { + tracing::warn!(username, "User is deactivated"); PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let ctx = AccountInactiveContext::new(user) @@ -301,6 +307,7 @@ pub(crate) async fn post( } if user.locked_at.is_some() { + tracing::warn!(username, "User is locked"); PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let ctx = AccountInactiveContext::new(user) @@ -424,7 +431,8 @@ mod test { header::{CONTENT_TYPE, LOCATION}, }; use mas_data_model::{ - UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod, + UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderOnBackchannelLogout, + UpstreamOAuthProviderTokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use mas_router::Route; @@ -500,6 +508,7 @@ mod test { additional_authorization_parameters: Vec::new(), forward_login_hint: false, ui_order: 0, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, ) .await @@ -542,6 +551,7 @@ mod test { additional_authorization_parameters: Vec::new(), forward_login_hint: false, ui_order: 1, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, ) .await diff --git a/crates/handlers/src/views/logout.rs b/crates/handlers/src/views/logout.rs index 66bd28311..731071c8b 100644 --- a/crates/handlers/src/views/logout.rs +++ b/crates/handlers/src/views/logout.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Form, State}, diff --git a/crates/handlers/src/views/mod.rs b/crates/handlers/src/views/mod.rs index 5d5c615e8..bc070cda7 100644 --- a/crates/handlers/src/views/mod.rs +++ b/crates/handlers/src/views/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub mod app; pub mod index; diff --git a/crates/handlers/src/views/recovery/mod.rs b/crates/handlers/src/views/recovery/mod.rs index fefae9890..630e9905d 100644 --- a/crates/handlers/src/views/recovery/mod.rs +++ b/crates/handlers/src/views/recovery/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub mod progress; pub mod start; diff --git a/crates/handlers/src/views/recovery/progress.rs b/crates/handlers/src/views/recovery/progress.rs index ea56e6cb1..75c342b1e 100644 --- a/crates/handlers/src/views/recovery/progress.rs +++ b/crates/handlers/src/views/recovery/progress.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ Form, diff --git a/crates/handlers/src/views/recovery/start.rs b/crates/handlers/src/views/recovery/start.rs index 72d0bc666..df52549b3 100644 --- a/crates/handlers/src/views/recovery/start.rs +++ b/crates/handlers/src/views/recovery/start.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::str::FromStr; diff --git a/crates/handlers/src/views/register/cookie.rs b/crates/handlers/src/views/register/cookie.rs index 7e3eb8173..d00d11109 100644 --- a/crates/handlers/src/views/register/cookie.rs +++ b/crates/handlers/src/views/register/cookie.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // TODO: move that to a standalone cookie manager diff --git a/crates/handlers/src/views/register/mod.rs b/crates/handlers/src/views/register/mod.rs index 3afe24573..f78a3df2e 100644 --- a/crates/handlers/src/views/register/mod.rs +++ b/crates/handlers/src/views/register/mod.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use axum::{ extract::{Query, State}, diff --git a/crates/handlers/src/views/register/password.rs b/crates/handlers/src/views/register/password.rs index 0745567a9..b7c29eaa1 100644 --- a/crates/handlers/src/views/register/password.rs +++ b/crates/handlers/src/views/register/password.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{str::FromStr, sync::Arc}; @@ -119,7 +119,7 @@ pub(crate) async fn get( } #[tracing::instrument(name = "handlers.views.password_register.post", skip_all)] -#[allow(clippy::too_many_lines, clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, diff --git a/crates/handlers/src/views/register/steps/display_name.rs b/crates/handlers/src/views/register/steps/display_name.rs index fa029475a..dd1a846d0 100644 --- a/crates/handlers/src/views/register/steps/display_name.rs +++ b/crates/handlers/src/views/register/steps/display_name.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use axum::{ diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index 55fb47e71..2bf0d36ee 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock}; diff --git a/crates/handlers/src/views/register/steps/mod.rs b/crates/handlers/src/views/register/steps/mod.rs index ae57f5a0c..715934d9f 100644 --- a/crates/handlers/src/views/register/steps/mod.rs +++ b/crates/handlers/src/views/register/steps/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub(crate) mod display_name; pub(crate) mod finish; diff --git a/crates/handlers/src/views/register/steps/registration_token.rs b/crates/handlers/src/views/register/steps/registration_token.rs index eacf343a3..9697214fe 100644 --- a/crates/handlers/src/views/register/steps/registration_token.rs +++ b/crates/handlers/src/views/register/steps/registration_token.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context as _; use axum::{ diff --git a/crates/handlers/src/views/register/steps/verify_email.rs b/crates/handlers/src/views/register/steps/verify_email.rs index bd291d5f0..bf464f8d6 100644 --- a/crates/handlers/src/views/register/steps/verify_email.rs +++ b/crates/handlers/src/views/register/steps/verify_email.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context; use axum::{ diff --git a/crates/handlers/src/views/shared.rs b/crates/handlers/src/views/shared.rs index 8a304ff83..05b494de8 100644 --- a/crates/handlers/src/views/shared.rs +++ b/crates/handlers/src/views/shared.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context; use mas_router::{PostAuthAction, Route, UrlBuilder}; diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index 3df605792..66eaac607 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-http" description = "HTTP utilities for the Matrix Authentication Service" diff --git a/crates/http/src/ext.rs b/crates/http/src/ext.rs index 838531df4..00c8a3013 100644 --- a/crates/http/src/ext.rs +++ b/crates/http/src/ext.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::OnceLock; diff --git a/crates/http/src/lib.rs b/crates/http/src/lib.rs index 72c64aaba..02d1864da 100644 --- a/crates/http/src/lib.rs +++ b/crates/http/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Utilities to do HTTP requests diff --git a/crates/http/src/reqwest.rs b/crates/http/src/reqwest.rs index 561fb100f..a399a7423 100644 --- a/crates/http/src/reqwest.rs +++ b/crates/http/src/reqwest.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ str::FromStr, @@ -91,7 +91,13 @@ impl reqwest::dns::Resolve for TracingResolver { #[must_use] pub fn client() -> reqwest::Client { // TODO: can/should we limit in-flight requests? - let tls_config = rustls::ClientConfig::with_platform_verifier(); + + // The explicit typing here is because `use_preconfigured_tls` accepts + // `Any`, but wants a `ClientConfig` under the hood. This helps us detect + // breaking changes in the rustls-platform-verifier API. + let tls_config: rustls::ClientConfig = + rustls::ClientConfig::with_platform_verifier().expect("failed to create TLS config"); + reqwest::Client::builder() .dns_resolver(Arc::new(TracingResolver::new())) .use_preconfigured_tls(tls_config) diff --git a/crates/i18n-scan/Cargo.toml b/crates/i18n-scan/Cargo.toml index c70d7b4e7..cfb1bd66b 100644 --- a/crates/i18n-scan/Cargo.toml +++ b/crates/i18n-scan/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-i18n-scan" version.workspace = true diff --git a/crates/i18n-scan/src/key.rs b/crates/i18n-scan/src/key.rs index 8f5663d07..2a3b276d8 100644 --- a/crates/i18n-scan/src/key.rs +++ b/crates/i18n-scan/src/key.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_i18n::{Message, translations::TranslationTree}; use minijinja::machinery::Span; diff --git a/crates/i18n-scan/src/main.rs b/crates/i18n-scan/src/main.rs index f514d9c09..7f0824c36 100644 --- a/crates/i18n-scan/src/main.rs +++ b/crates/i18n-scan/src/main.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // Without the custom_syntax feature, the `SyntaxConfig` is a unit struct // which is annoying with this clippy lint diff --git a/crates/i18n-scan/src/minijinja.rs b/crates/i18n-scan/src/minijinja.rs index 51910783a..b6e9dda51 100644 --- a/crates/i18n-scan/src/minijinja.rs +++ b/crates/i18n-scan/src/minijinja.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub use minijinja::machinery::parse; use minijinja::{ diff --git a/crates/i18n/Cargo.toml b/crates/i18n/Cargo.toml index c73448060..8d04dc731 100644 --- a/crates/i18n/Cargo.toml +++ b/crates/i18n/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-i18n" version.workspace = true diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs index 44fb06a5e..a4dfc7e2a 100644 --- a/crates/i18n/src/lib.rs +++ b/crates/i18n/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub mod sprintf; pub mod translations; diff --git a/crates/i18n/src/sprintf/argument.rs b/crates/i18n/src/sprintf/argument.rs index 486aee58f..c83ac0e2d 100644 --- a/crates/i18n/src/sprintf/argument.rs +++ b/crates/i18n/src/sprintf/argument.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/i18n/src/sprintf/formatter.rs b/crates/i18n/src/sprintf/formatter.rs index a0846e46f..29c71f6aa 100644 --- a/crates/i18n/src/sprintf/formatter.rs +++ b/crates/i18n/src/sprintf/formatter.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::fmt::Formatter; @@ -226,7 +226,7 @@ fn to_precision(number: f64, mut placeholder: Placeholder) -> String { } } -#[allow(clippy::too_many_lines, clippy::match_same_arms)] +#[allow(clippy::match_same_arms)] fn format_value(value: &Value, placeholder: &Placeholder) -> Result { match (value, &placeholder.type_specifier) { (Value::Number(number), ts @ TypeSpecifier::BinaryNumber) => { diff --git a/crates/i18n/src/sprintf/grammar.pest b/crates/i18n/src/sprintf/grammar.pest index 8620b0edd..6d33ad7f3 100644 --- a/crates/i18n/src/sprintf/grammar.pest +++ b/crates/i18n/src/sprintf/grammar.pest @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. message = _{ (text | percent | placeholder)* ~ EOI } diff --git a/crates/i18n/src/sprintf/message.rs b/crates/i18n/src/sprintf/message.rs index e78e4f908..f92041ac5 100644 --- a/crates/i18n/src/sprintf/message.rs +++ b/crates/i18n/src/sprintf/message.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use serde::{Deserialize, Serialize}; diff --git a/crates/i18n/src/sprintf/mod.rs b/crates/i18n/src/sprintf/mod.rs index 7fd062204..72ca75373 100644 --- a/crates/i18n/src/sprintf/mod.rs +++ b/crates/i18n/src/sprintf/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(unused_macros)] diff --git a/crates/i18n/src/sprintf/parser.rs b/crates/i18n/src/sprintf/parser.rs index cadb23ed7..ab5aaf87b 100644 --- a/crates/i18n/src/sprintf/parser.rs +++ b/crates/i18n/src/sprintf/parser.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::result_large_err)] diff --git a/crates/i18n/src/translations.rs b/crates/i18n/src/translations.rs index 8e90b5204..9bde8fa05 100644 --- a/crates/i18n/src/translations.rs +++ b/crates/i18n/src/translations.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::{BTreeMap, BTreeSet}, diff --git a/crates/i18n/src/translator.rs b/crates/i18n/src/translator.rs index 68afb1793..cd36a6c4b 100644 --- a/crates/i18n/src/translator.rs +++ b/crates/i18n/src/translator.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, fs::File, io::BufReader, str::FromStr}; diff --git a/crates/iana-codegen/Cargo.toml b/crates/iana-codegen/Cargo.toml index ae8f8d1ed..0f30c8c31 100644 --- a/crates/iana-codegen/Cargo.toml +++ b/crates/iana-codegen/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-iana-codegen" version.workspace = true diff --git a/crates/iana-codegen/src/generation.rs b/crates/iana-codegen/src/generation.rs index 82f67b05b..3ee792df1 100644 --- a/crates/iana-codegen/src/generation.rs +++ b/crates/iana-codegen/src/generation.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use crate::traits::{EnumMember, Section}; @@ -169,7 +169,6 @@ pub fn json_schema_impl( "{}".to_owned() }} - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {{ let enums = vec!["#, section.key, section.key, diff --git a/crates/iana-codegen/src/jose.rs b/crates/iana-codegen/src/jose.rs index 9f94cdf81..36d00de74 100644 --- a/crates/iana-codegen/src/jose.rs +++ b/crates/iana-codegen/src/jose.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use serde::Deserialize; diff --git a/crates/iana-codegen/src/main.rs b/crates/iana-codegen/src/main.rs index b5b1c28eb..cf809a0a9 100644 --- a/crates/iana-codegen/src/main.rs +++ b/crates/iana-codegen/src/main.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, fmt::Display}; @@ -71,15 +71,14 @@ impl File { } impl Display for File { - #[allow(clippy::too_many_lines)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!( f, - r"// Copyright 2024 New Vector Ltd. + r"// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::doc_markdown)] diff --git a/crates/iana-codegen/src/oauth.rs b/crates/iana-codegen/src/oauth.rs index 9afc62659..2809bb7db 100644 --- a/crates/iana-codegen/src/oauth.rs +++ b/crates/iana-codegen/src/oauth.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use serde::Deserialize; diff --git a/crates/iana-codegen/src/traits.rs b/crates/iana-codegen/src/traits.rs index c080f901c..c5a2617e6 100644 --- a/crates/iana-codegen/src/traits.rs +++ b/crates/iana-codegen/src/traits.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context; use async_trait::async_trait; diff --git a/crates/iana/Cargo.toml b/crates/iana/Cargo.toml index 796abe4ba..d646bac19 100644 --- a/crates/iana/Cargo.toml +++ b/crates/iana/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-iana" description = "IANA registry data for JOSE and OAuth 2.0" diff --git a/crates/iana/src/jose.rs b/crates/iana/src/jose.rs index 8ffc395ca..666d6cd7f 100644 --- a/crates/iana/src/jose.rs +++ b/crates/iana/src/jose.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::doc_markdown)] @@ -148,7 +148,6 @@ impl schemars::JsonSchema for JsonWebSignatureAlg { "JsonWebSignatureAlg".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -537,7 +536,6 @@ impl schemars::JsonSchema for JsonWebEncryptionAlg { "JsonWebEncryptionAlg".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -887,7 +885,6 @@ impl schemars::JsonSchema for JsonWebEncryptionEnc { "JsonWebEncryptionEnc".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -1043,7 +1040,6 @@ impl schemars::JsonSchema for JsonWebEncryptionCompressionAlgorithm { "JsonWebEncryptionCompressionAlgorithm".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -1149,7 +1145,6 @@ impl schemars::JsonSchema for JsonWebKeyType { "JsonWebKeyType".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -1294,7 +1289,6 @@ impl schemars::JsonSchema for JsonWebKeyEcEllipticCurve { "JsonWebKeyEcEllipticCurve".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -1439,7 +1433,6 @@ impl schemars::JsonSchema for JsonWebKeyOkpEllipticCurve { "JsonWebKeyOkpEllipticCurve".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -1574,7 +1567,6 @@ impl schemars::JsonSchema for JsonWebKeyUse { "JsonWebKeyUse".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -1713,7 +1705,6 @@ impl schemars::JsonSchema for JsonWebKeyOperation { "JsonWebKeyOperation".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- diff --git a/crates/iana/src/lib.rs b/crates/iana/src/lib.rs index 59f8b3bf1..657e7375c 100644 --- a/crates/iana/src/lib.rs +++ b/crates/iana/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Values from IANA registries, generated by the `mas-iana-codegen` crate diff --git a/crates/iana/src/oauth.rs b/crates/iana/src/oauth.rs index 530e4bd7b..3b65ce9f2 100644 --- a/crates/iana/src/oauth.rs +++ b/crates/iana/src/oauth.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::doc_markdown)] @@ -83,7 +83,6 @@ impl schemars::JsonSchema for OAuthAccessTokenType { "OAuthAccessTokenType".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -215,7 +214,6 @@ impl schemars::JsonSchema for OAuthAuthorizationEndpointResponseType { "OAuthAuthorizationEndpointResponseType".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -351,7 +349,6 @@ impl schemars::JsonSchema for OAuthTokenTypeHint { "OAuthTokenTypeHint".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -477,7 +474,6 @@ impl schemars::JsonSchema for OAuthClientAuthenticationMethod { "OAuthClientAuthenticationMethod".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- @@ -602,7 +598,6 @@ impl schemars::JsonSchema for PkceCodeChallengeMethod { "PkceCodeChallengeMethod".to_owned() } - #[allow(clippy::too_many_lines)] fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { let enums = vec![ // --- diff --git a/crates/jose/Cargo.toml b/crates/jose/Cargo.toml index 9ca221bb3..ac9c4d96a 100644 --- a/crates/jose/Cargo.toml +++ b/crates/jose/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-jose" description = "JSON Object Signing and Encryption (JWT & co) utilities" diff --git a/crates/jose/src/base64.rs b/crates/jose/src/base64.rs index b5b0b2386..5886368ab 100644 --- a/crates/jose/src/base64.rs +++ b/crates/jose/src/base64.rs @@ -1,3 +1,8 @@ +// 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. + //! Transparent base64 encoding / decoding as part of (de)serialization. use std::{borrow::Cow, fmt, marker::PhantomData, str}; diff --git a/crates/jose/src/claims.rs b/crates/jose/src/claims.rs index 455be2ff9..3ab6e26e0 100644 --- a/crates/jose/src/claims.rs +++ b/crates/jose/src/claims.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, convert::Infallible, marker::PhantomData, ops::Deref}; @@ -49,7 +49,7 @@ impl Validator for () { } pub struct Claim { - claim: &'static str, + value: &'static str, t: PhantomData, v: PhantomData, } @@ -61,7 +61,7 @@ where #[must_use] pub const fn new(claim: &'static str) -> Self { Self { - claim, + value: claim, t: PhantomData, v: PhantomData, } @@ -83,8 +83,8 @@ where { let value = value.into(); let value: serde_json::Value = - serde_json::to_value(&value).map_err(|_| ClaimError::InvalidClaim(self.claim))?; - claims.insert(self.claim.to_owned(), value); + serde_json::to_value(&value).map_err(|_| ClaimError::InvalidClaim(self.value))?; + claims.insert(self.value.to_owned(), value); Ok(()) } @@ -126,15 +126,15 @@ where { let validator: V = validator.into(); let claim = claims - .remove(self.claim) - .ok_or(ClaimError::MissingClaim(self.claim))?; + .remove(self.value) + .ok_or(ClaimError::MissingClaim(self.value))?; let res = - serde_json::from_value(claim).map_err(|_| ClaimError::InvalidClaim(self.claim))?; + serde_json::from_value(claim).map_err(|_| ClaimError::InvalidClaim(self.value))?; validator .validate(&res) .map_err(|source| ClaimError::ValidationError { - claim: self.claim, + claim: self.value, source: Box::new(source), })?; Ok(res) @@ -182,6 +182,22 @@ where Err(e) => Err(e), } } + + /// Assert that the claim is absent. + /// + /// # Errors + /// + /// Returns an error if the claim is present. + pub fn assert_absent( + &self, + claims: &HashMap, + ) -> Result<(), ClaimError> { + if claims.contains_key(self.value) { + Err(ClaimError::InvalidClaim(self.value)) + } else { + Ok(()) + } + } } #[derive(Debug, Clone)] @@ -525,7 +541,15 @@ mod oidc_core { pub const UPDATED_AT: Claim = Claim::new("updated_at"); } -pub use self::{oidc_core::*, rfc7519::*}; +/// Claims defined in OpenID.FrontChannel +/// +mod oidc_frontchannel { + use super::Claim; + + pub const SID: Claim = Claim::new("sid"); +} + +pub use self::{oidc_core::*, oidc_frontchannel::*, rfc7519::*}; #[cfg(test)] mod tests { diff --git a/crates/jose/src/constraints.rs b/crates/jose/src/constraints.rs index 23e0ec712..16859c889 100644 --- a/crates/jose/src/constraints.rs +++ b/crates/jose/src/constraints.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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; diff --git a/crates/jose/src/jwa/asymmetric.rs b/crates/jose/src/jwa/asymmetric.rs index 21472deb6..b490d5796 100644 --- a/crates/jose/src/jwa/asymmetric.rs +++ b/crates/jose/src/jwa/asymmetric.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use digest::Digest; use mas_iana::jose::{JsonWebKeyEcEllipticCurve, JsonWebSignatureAlg}; diff --git a/crates/jose/src/jwa/hmac.rs b/crates/jose/src/jwa/hmac.rs index af7bd59d9..ba24ee960 100644 --- a/crates/jose/src/jwa/hmac.rs +++ b/crates/jose/src/jwa/hmac.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::marker::PhantomData; diff --git a/crates/jose/src/jwa/mod.rs b/crates/jose/src/jwa/mod.rs index 690b57993..f01308575 100644 --- a/crates/jose/src/jwa/mod.rs +++ b/crates/jose/src/jwa/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_iana::jose::JsonWebSignatureAlg; use sha2::{Sha256, Sha384, Sha512}; diff --git a/crates/jose/src/jwa/signature.rs b/crates/jose/src/jwa/signature.rs index af72f3fcc..35c3cb978 100644 --- a/crates/jose/src/jwa/signature.rs +++ b/crates/jose/src/jwa/signature.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use signature::SignatureEncoding as _; diff --git a/crates/jose/src/jwa/symmetric.rs b/crates/jose/src/jwa/symmetric.rs index b5d821dbc..3e45b6ab0 100644 --- a/crates/jose/src/jwa/symmetric.rs +++ b/crates/jose/src/jwa/symmetric.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_iana::jose::JsonWebSignatureAlg; use thiserror::Error; diff --git a/crates/jose/src/jwk/mod.rs b/crates/jose/src/jwk/mod.rs index 8c397239c..98d40fa7a 100644 --- a/crates/jose/src/jwk/mod.rs +++ b/crates/jose/src/jwk/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Ref: @@ -413,7 +413,6 @@ mod tests { assert_eq!(candidates.len(), 1); } - #[allow(clippy::too_many_lines)] #[test] fn load_keycloak_keys() { let jwks = serde_json::json!({ diff --git a/crates/jose/src/jwk/private_parameters.rs b/crates/jose/src/jwk/private_parameters.rs index d0ac8be87..6518e5ba4 100644 --- a/crates/jose/src/jwk/private_parameters.rs +++ b/crates/jose/src/jwk/private_parameters.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_iana::jose::{ JsonWebKeyEcEllipticCurve, JsonWebKeyOkpEllipticCurve, JsonWebKeyType, JsonWebSignatureAlg, diff --git a/crates/jose/src/jwk/public_parameters.rs b/crates/jose/src/jwk/public_parameters.rs index 1b5eaadbf..1bc29604d 100644 --- a/crates/jose/src/jwk/public_parameters.rs +++ b/crates/jose/src/jwk/public_parameters.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_iana::jose::{ JsonWebKeyEcEllipticCurve, JsonWebKeyOkpEllipticCurve, JsonWebKeyType, JsonWebSignatureAlg, diff --git a/crates/jose/src/jwt/header.rs b/crates/jose/src/jwt/header.rs index 56aaef6d8..c35e9fdb4 100644 --- a/crates/jose/src/jwt/header.rs +++ b/crates/jose/src/jwt/header.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_iana::jose::JsonWebSignatureAlg; use serde::{Deserialize, Serialize}; diff --git a/crates/jose/src/jwt/mod.rs b/crates/jose/src/jwt/mod.rs index 15cf20f2f..50a777195 100644 --- a/crates/jose/src/jwt/mod.rs +++ b/crates/jose/src/jwt/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod header; mod raw; diff --git a/crates/jose/src/jwt/raw.rs b/crates/jose/src/jwt/raw.rs index 869150346..46193f993 100644 --- a/crates/jose/src/jwt/raw.rs +++ b/crates/jose/src/jwt/raw.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{borrow::Cow, ops::Deref}; diff --git a/crates/jose/src/jwt/signed.rs b/crates/jose/src/jwt/signed.rs index ba7a552c8..3d632c05b 100644 --- a/crates/jose/src/jwt/signed.rs +++ b/crates/jose/src/jwt/signed.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use base64ct::{Base64UrlUnpadded, Encoding}; use rand::thread_rng; diff --git a/crates/jose/src/lib.rs b/crates/jose/src/lib.rs index 9f9bf2348..3384d9f1c 100644 --- a/crates/jose/src/lib.rs +++ b/crates/jose/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(rustdoc::broken_intra_doc_links)] #![allow(clippy::module_name_repetitions)] diff --git a/crates/jose/tests/generate.py b/crates/jose/tests/generate.py index d66f55369..e70f5725c 100644 --- a/crates/jose/tests/generate.py +++ b/crates/jose/tests/generate.py @@ -1,3 +1,8 @@ +# 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. + # Generates test keys, JWKS and JWTs # Required the `openssl` binary and the `authlib` python library diff --git a/crates/jose/tests/jws.rs b/crates/jose/tests/jws.rs index e6947d5a0..d7068cdad 100644 --- a/crates/jose/tests/jws.rs +++ b/crates/jose/tests/jws.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. static HS256_JWT: &str = include_str!("./jwts/hs256.jwt"); static HS384_JWT: &str = include_str!("./jwts/hs384.jwt"); diff --git a/crates/keystore/Cargo.toml b/crates/keystore/Cargo.toml index 668d555ad..0b97e5d88 100644 --- a/crates/keystore/Cargo.toml +++ b/crates/keystore/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-keystore" description = "Secret keys store used by the Matrix Authentication Service" diff --git a/crates/keystore/src/encrypter.rs b/crates/keystore/src/encrypter.rs index d4d61154d..a2cb738e0 100644 --- a/crates/keystore/src/encrypter.rs +++ b/crates/keystore/src/encrypter.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; diff --git a/crates/keystore/src/lib.rs b/crates/keystore/src/lib.rs index 639afcb0d..66de683f0 100644 --- a/crates/keystore/src/lib.rs +++ b/crates/keystore/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A crate to store keys which can then be used to sign and verify JWTs. diff --git a/crates/keystore/tests/generate.sh b/crates/keystore/tests/generate.sh index c2de69c9f..b6cbe1082 100644 --- a/crates/keystore/tests/generate.sh +++ b/crates/keystore/tests/generate.sh @@ -1,4 +1,8 @@ #!/bin/sh +# 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. set -eux diff --git a/crates/keystore/tests/keystore.rs b/crates/keystore/tests/keystore.rs index 6bdbe8286..08ad2ddc2 100644 --- a/crates/keystore/tests/keystore.rs +++ b/crates/keystore/tests/keystore.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use der::pem::LineEnding; use mas_iana::jose::JsonWebSignatureAlg; diff --git a/crates/listener/Cargo.toml b/crates/listener/Cargo.toml index 14d84ea9b..5171e86b4 100644 --- a/crates/listener/Cargo.toml +++ b/crates/listener/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-listener" version.workspace = true diff --git a/crates/listener/examples/demo/certs/gen.sh b/crates/listener/examples/demo/certs/gen.sh index 4ce16dcc3..019279ec1 100644 --- a/crates/listener/examples/demo/certs/gen.sh +++ b/crates/listener/examples/demo/certs/gen.sh @@ -1,4 +1,8 @@ #!/bin/sh +# 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. # Script to regenerate the server and client certificate diff --git a/crates/listener/examples/demo/main.rs b/crates/listener/examples/demo/main.rs index 1a950fa6d..2a0462309 100644 --- a/crates/listener/examples/demo/main.rs +++ b/crates/listener/examples/demo/main.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ convert::Infallible, diff --git a/crates/listener/src/lib.rs b/crates/listener/src/lib.rs index 74ded2388..1618365b1 100644 --- a/crates/listener/src/lib.rs +++ b/crates/listener/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(rustdoc::missing_crate_level_docs)] #![allow(clippy::module_name_repetitions)] diff --git a/crates/listener/src/maybe_tls.rs b/crates/listener/src/maybe_tls.rs index 635141cbc..02fec8946 100644 --- a/crates/listener/src/maybe_tls.rs +++ b/crates/listener/src/maybe_tls.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ pin::Pin, diff --git a/crates/listener/src/proxy_protocol/acceptor.rs b/crates/listener/src/proxy_protocol/acceptor.rs index 7ca24bd56..d62ab6182 100644 --- a/crates/listener/src/proxy_protocol/acceptor.rs +++ b/crates/listener/src/proxy_protocol/acceptor.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use bytes::BytesMut; use thiserror::Error; diff --git a/crates/listener/src/proxy_protocol/maybe.rs b/crates/listener/src/proxy_protocol/maybe.rs index 29e47f047..47c4860e8 100644 --- a/crates/listener/src/proxy_protocol/maybe.rs +++ b/crates/listener/src/proxy_protocol/maybe.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use tokio::io::AsyncRead; diff --git a/crates/listener/src/proxy_protocol/mod.rs b/crates/listener/src/proxy_protocol/mod.rs index 820ef428a..5f9929fed 100644 --- a/crates/listener/src/proxy_protocol/mod.rs +++ b/crates/listener/src/proxy_protocol/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod acceptor; mod maybe; diff --git a/crates/listener/src/proxy_protocol/v1.rs b/crates/listener/src/proxy_protocol/v1.rs index 93a5dabd1..bb78aa654 100644 --- a/crates/listener/src/proxy_protocol/v1.rs +++ b/crates/listener/src/proxy_protocol/v1.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ net::{AddrParseError, Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -52,7 +52,6 @@ impl ParseError { } impl ProxyProtocolV1Info { - #[allow(clippy::too_many_lines)] pub(super) fn parse(buf: &mut B) -> Result where B: Buf + AsRef<[u8]>, diff --git a/crates/listener/src/server.rs b/crates/listener/src/server.rs index f0b66820d..026706a45 100644 --- a/crates/listener/src/server.rs +++ b/crates/listener/src/server.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::{ pin::Pin, @@ -290,7 +290,6 @@ where } } -#[allow(clippy::too_many_lines)] pub async fn run_servers( listeners: impl IntoIterator>, soft_shutdown_token: CancellationToken, diff --git a/crates/listener/src/unix_or_tcp.rs b/crates/listener/src/unix_or_tcp.rs index 565adec4d..5cd85c441 100644 --- a/crates/listener/src/unix_or_tcp.rs +++ b/crates/listener/src/unix_or_tcp.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A listener which can listen on either TCP sockets or on UNIX domain sockets @@ -152,7 +152,6 @@ impl UnixOrTcpListener { let socket = socket2::SockRef::from(&stream); socket.set_keepalive(true)?; - socket.set_nodelay(true)?; Ok((remote_addr.into(), UnixOrTcpConnection::Unix { stream })) } @@ -161,7 +160,7 @@ impl UnixOrTcpListener { let socket = socket2::SockRef::from(&stream); socket.set_keepalive(true)?; - socket.set_nodelay(true)?; + socket.set_tcp_nodelay(true)?; Ok((remote_addr.into(), UnixOrTcpConnection::Tcp { stream })) } @@ -188,7 +187,6 @@ impl UnixOrTcpListener { let socket = socket2::SockRef::from(&stream); socket.set_keepalive(true)?; - socket.set_nodelay(true)?; Poll::Ready(Ok(( remote_addr.into(), @@ -200,7 +198,7 @@ impl UnixOrTcpListener { let socket = socket2::SockRef::from(&stream); socket.set_keepalive(true)?; - socket.set_nodelay(true)?; + socket.set_tcp_nodelay(true)?; Poll::Ready(Ok(( remote_addr.into(), diff --git a/crates/matrix-synapse/Cargo.toml b/crates/matrix-synapse/Cargo.toml index 8e3e1841c..34c08e9c9 100644 --- a/crates/matrix-synapse/Cargo.toml +++ b/crates/matrix-synapse/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-matrix-synapse" version.workspace = true diff --git a/crates/matrix-synapse/src/error.rs b/crates/matrix-synapse/src/error.rs index bcb31157b..c1d98ccd1 100644 --- a/crates/matrix-synapse/src/error.rs +++ b/crates/matrix-synapse/src/error.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::fmt::Display; @@ -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..b93298ceb --- /dev/null +++ b/crates/matrix-synapse/src/legacy.rs @@ -0,0 +1,688 @@ +// 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.verify_token", skip_all, err(Debug))] + async fn verify_token(&self, token: &str) -> Result { + Ok(self.access_token == token) + } + + #[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 5ca7daa9e..062ecaa75 100644 --- a/crates/matrix-synapse/src/lib.rs +++ b/crates/matrix-synapse/src/lib.rs @@ -1,677 +1,11 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE 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"; +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. 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) => - { - 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..3d70f52de --- /dev/null +++ b/crates/matrix-synapse/src/modern.rs @@ -0,0 +1,567 @@ +// 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.verify_token", skip_all, err(Debug))] + async fn verify_token(&self, token: &str) -> Result { + Ok(self.access_token == token) + } + + #[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/Cargo.toml b/crates/matrix/Cargo.toml index 2182c6426..a041fee5e 100644 --- a/crates/matrix/Cargo.toml +++ b/crates/matrix/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-matrix" version.workspace = true diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index ae8a4e563..f1fbe9c83 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod mock; mod readonly; @@ -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. @@ -207,17 +207,31 @@ pub trait HomeserverConnection: Send + Sync { Some(mxid.localpart()) } + /// Verify a bearer token coming from the homeserver for homeserver to MAS + /// interactions + /// + /// Returns `true` if the token is valid, `false` otherwise. + /// + /// # Parameters + /// + /// * `token` - The token to verify. + /// + /// # Errors + /// + /// Returns an error if the token failed to verify. + async fn verify_token(&self, token: &str) -> Result; + /// Query the state of a user on the homeserver. /// /// # 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 +261,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 +279,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 +289,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 +298,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 +398,12 @@ impl HomeserverConnection for &T (**self).homeserver() } - async fn query_user(&self, mxid: &str) -> Result { - (**self).query_user(mxid).await + async fn verify_token(&self, token: &str) -> Result { + (**self).verify_token(token).await + } + + async fn query_user(&self, localpart: &str) -> Result { + (**self).query_user(localpart).await } async fn provision_user(&self, request: &ProvisionRequest) -> Result { @@ -388,58 +414,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 +480,12 @@ impl HomeserverConnection for Arc { (**self).homeserver() } - async fn query_user(&self, mxid: &str) -> Result { - (**self).query_user(mxid).await + async fn verify_token(&self, token: &str) -> Result { + (**self).verify_token(token).await + } + + async fn query_user(&self, localpart: &str) -> Result { + (**self).query_user(localpart).await } async fn provision_user(&self, request: &ProvisionRequest) -> Result { @@ -462,57 +496,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 7c7973ce0..4180597e2 100644 --- a/crates/matrix/src/mock.rs +++ b/crates/matrix/src/mock.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::{HashMap, HashSet}; @@ -31,6 +31,10 @@ pub struct HomeserverConnection { } impl HomeserverConnection { + /// A valid bearer token that will be accepted by + /// [`crate::HomeserverConnection::verify_token`]. + pub const VALID_BEARER_TOKEN: &str = "mock_homeserver_bearer_token"; + /// Create a new mock connection. pub fn new(homeserver: H) -> Self where @@ -54,9 +58,14 @@ impl crate::HomeserverConnection for HomeserverConnection { &self.homeserver } - async fn query_user(&self, mxid: &str) -> Result { + async fn verify_token(&self, token: &str) -> Result { + Ok(token == Self::VALID_BEARER_TOKEN) + } + + 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 +75,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 +117,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 +178,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 +230,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 +242,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 530c3cd89..590583bf8 100644 --- a/crates/matrix/src/readonly.rs +++ b/crates/matrix/src/readonly.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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; @@ -28,8 +28,12 @@ impl HomeserverConnection for ReadOnlyHomeserverConnect self.inner.homeserver() } - async fn query_user(&self, mxid: &str) -> Result { - self.inner.query_user(mxid).await + async fn verify_token(&self, token: &str) -> Result { + self.inner.verify_token(token).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 +44,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 +55,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/oauth2-types/Cargo.toml b/crates/oauth2-types/Cargo.toml index 4707bb137..2432e34f5 100644 --- a/crates/oauth2-types/Cargo.toml +++ b/crates/oauth2-types/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "oauth2-types" description = "OAuth 2.0 types used by the Matrix Authentication Service" diff --git a/crates/oauth2-types/src/errors.rs b/crates/oauth2-types/src/errors.rs index f5e349c82..b3077636c 100644 --- a/crates/oauth2-types/src/errors.rs +++ b/crates/oauth2-types/src/errors.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Error types returned by an authorization server. diff --git a/crates/oauth2-types/src/lib.rs b/crates/oauth2-types/src/lib.rs index 0f1729ec8..e937288ff 100644 --- a/crates/oauth2-types/src/lib.rs +++ b/crates/oauth2-types/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! [OAuth 2.0] and [OpenID Connect] types. //! diff --git a/crates/oauth2-types/src/oidc.rs b/crates/oauth2-types/src/oidc.rs index 5cbdf2e4b..a9befbea1 100644 --- a/crates/oauth2-types/src/oidc.rs +++ b/crates/oauth2-types/src/oidc.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Types to interact with the [OpenID Connect] specification. //! @@ -647,7 +647,10 @@ impl ProviderMetadata { let metadata = self.insecure_verify_metadata()?; if metadata.issuer() != issuer { - return Err(ProviderMetadataVerificationError::IssuerUrlsDontMatch); + return Err(ProviderMetadataVerificationError::IssuerUrlsDontMatch { + expected: issuer.to_owned(), + actual: metadata.issuer().to_owned(), + }); } validate_url( @@ -1064,8 +1067,13 @@ pub enum ProviderMetadataVerificationError { UrlWithFragment(&'static str, Url), /// The issuer URL doesn't match the one that was discovered. - #[error("issuer URLs don't match")] - IssuerUrlsDontMatch, + #[error("issuer URLs don't match: expected {expected:?}, got {actual:?}")] + IssuerUrlsDontMatch { + /// The expected issuer URL. + expected: String, + /// The issuer URL that was discovered. + actual: String, + }, /// `openid` is missing from the supported scopes. #[error("missing openid scope")] @@ -1314,7 +1322,7 @@ mod tests { metadata.issuer = Some("https://example.com/".to_owned()); assert_matches!( metadata.clone().validate(&issuer), - Err(ProviderMetadataVerificationError::IssuerUrlsDontMatch) + Err(ProviderMetadataVerificationError::IssuerUrlsDontMatch { .. }) ); // Err - Not https diff --git a/crates/oauth2-types/src/pkce.rs b/crates/oauth2-types/src/pkce.rs index 0682cb149..d806ae7f6 100644 --- a/crates/oauth2-types/src/pkce.rs +++ b/crates/oauth2-types/src/pkce.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Types for the [Proof Key for Code Exchange]. //! diff --git a/crates/oauth2-types/src/registration/client_metadata_serde.rs b/crates/oauth2-types/src/registration/client_metadata_serde.rs index 8ccefe36f..8edfcc6ca 100644 --- a/crates/oauth2-types/src/registration/client_metadata_serde.rs +++ b/crates/oauth2-types/src/registration/client_metadata_serde.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::borrow::Cow; diff --git a/crates/oauth2-types/src/registration/mod.rs b/crates/oauth2-types/src/registration/mod.rs index 27d99c2a8..fd1ab2a64 100644 --- a/crates/oauth2-types/src/registration/mod.rs +++ b/crates/oauth2-types/src/registration/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Types for [Dynamic Client Registration]. //! @@ -440,7 +440,6 @@ impl ClientMetadata { /// Will return `Err` if validation fails. /// /// [OpenID Connect Dynamic Client Registration Spec 1.0]: https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata - #[allow(clippy::too_many_lines)] pub fn validate(self) -> Result { let grant_types = self.grant_types(); let has_implicit = grant_types.contains(&GrantType::Implicit); @@ -994,7 +993,6 @@ mod tests { } #[test] - #[allow(clippy::too_many_lines)] fn validate_response_types() { let mut metadata = valid_client_metadata(); diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index 36ee36da6..ac0770411 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests and response types to interact with the [OAuth 2.0] specification. //! diff --git a/crates/oauth2-types/src/response_type.rs b/crates/oauth2-types/src/response_type.rs index 1b4c4da03..1f3322e94 100644 --- a/crates/oauth2-types/src/response_type.rs +++ b/crates/oauth2-types/src/response_type.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! [Response types] in the OpenID Connect specification. //! @@ -272,7 +272,6 @@ mod tests { } #[test] - #[allow(clippy::too_many_lines)] fn deserialize_response_type() { serde_json::from_str::("\"\"").unwrap_err(); diff --git a/crates/oauth2-types/src/scope.rs b/crates/oauth2-types/src/scope.rs index c758a8032..f9832b5c0 100644 --- a/crates/oauth2-types/src/scope.rs +++ b/crates/oauth2-types/src/scope.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Types to define an [access token's scope]. //! diff --git a/crates/oauth2-types/src/test_utils.rs b/crates/oauth2-types/src/test_utils.rs index a1bdb30d5..69e58f9ea 100644 --- a/crates/oauth2-types/src/test_utils.rs +++ b/crates/oauth2-types/src/test_utils.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::fmt::Debug; diff --git a/crates/oauth2-types/src/webfinger.rs b/crates/oauth2-types/src/webfinger.rs index 17d7557c4..34e132324 100644 --- a/crates/oauth2-types/src/webfinger.rs +++ b/crates/oauth2-types/src/webfinger.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Types for provider discovery using [Webfinger]. //! diff --git a/crates/oidc-client/Cargo.toml b/crates/oidc-client/Cargo.toml index 5b35ec913..3fffd584c 100644 --- a/crates/oidc-client/Cargo.toml +++ b/crates/oidc-client/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-oidc-client" description = "OpenID Connect client library used by the Matrix Authentication Service" diff --git a/crates/oidc-client/src/error.rs b/crates/oidc-client/src/error.rs index 1642054e7..0f424464f 100644 --- a/crates/oidc-client/src/error.rs +++ b/crates/oidc-client/src/error.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! The error types used in this crate. diff --git a/crates/oidc-client/src/lib.rs b/crates/oidc-client/src/lib.rs index 24da41909..2b35896be 100644 --- a/crates/oidc-client/src/lib.rs +++ b/crates/oidc-client/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! An [OpenID Connect] client library for the [Matrix] specification. //! diff --git a/crates/oidc-client/src/requests/authorization_code.rs b/crates/oidc-client/src/requests/authorization_code.rs index 9271fe33c..4965e13d0 100644 --- a/crates/oidc-client/src/requests/authorization_code.rs +++ b/crates/oidc-client/src/requests/authorization_code.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests for the [Authorization Code flow]. //! diff --git a/crates/oidc-client/src/requests/client_credentials.rs b/crates/oidc-client/src/requests/client_credentials.rs index aebf412b8..539d1a715 100644 --- a/crates/oidc-client/src/requests/client_credentials.rs +++ b/crates/oidc-client/src/requests/client_credentials.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests for the [Client Credentials flow]. //! diff --git a/crates/oidc-client/src/requests/discovery.rs b/crates/oidc-client/src/requests/discovery.rs index 1d724b553..e3807c412 100644 --- a/crates/oidc-client/src/requests/discovery.rs +++ b/crates/oidc-client/src/requests/discovery.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests for OpenID Connect Provider [Discovery]. //! diff --git a/crates/oidc-client/src/requests/jose.rs b/crates/oidc-client/src/requests/jose.rs index a4c858359..12915caab 100644 --- a/crates/oidc-client/src/requests/jose.rs +++ b/crates/oidc-client/src/requests/jose.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests and method related to JSON Object Signing and Encryption. diff --git a/crates/oidc-client/src/requests/mod.rs b/crates/oidc-client/src/requests/mod.rs index 757b3d527..24cc9e27d 100644 --- a/crates/oidc-client/src/requests/mod.rs +++ b/crates/oidc-client/src/requests/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Methods to interact with OpenID Connect and OAuth2.0 endpoints. diff --git a/crates/oidc-client/src/requests/refresh_token.rs b/crates/oidc-client/src/requests/refresh_token.rs index 8b9e7719a..9af088b0c 100644 --- a/crates/oidc-client/src/requests/refresh_token.rs +++ b/crates/oidc-client/src/requests/refresh_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests for using [Refresh Tokens]. //! diff --git a/crates/oidc-client/src/requests/token.rs b/crates/oidc-client/src/requests/token.rs index 38e988495..774ab93dd 100644 --- a/crates/oidc-client/src/requests/token.rs +++ b/crates/oidc-client/src/requests/token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests for the Token endpoint. diff --git a/crates/oidc-client/src/requests/userinfo.rs b/crates/oidc-client/src/requests/userinfo.rs index fbcd27039..e10526fcf 100644 --- a/crates/oidc-client/src/requests/userinfo.rs +++ b/crates/oidc-client/src/requests/userinfo.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Requests for obtaining [Claims] about an end-user. //! diff --git a/crates/oidc-client/src/types/client_credentials.rs b/crates/oidc-client/src/types/client_credentials.rs index ca8d2a8f6..a097ef469 100644 --- a/crates/oidc-client/src/types/client_credentials.rs +++ b/crates/oidc-client/src/types/client_credentials.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Types and methods for client credentials. @@ -130,7 +130,6 @@ impl ClientCredentials { /// Apply these [`ClientCredentials`] to the given request with the given /// form. - #[allow(clippy::too_many_lines)] pub(crate) fn authenticated_form( &self, request: reqwest::RequestBuilder, @@ -141,7 +140,7 @@ impl ClientCredentials { let request = match self { ClientCredentials::None { client_id } => request.form(&RequestWithClientCredentials { body: form, - client_id, + client_id: Some(client_id), client_secret: None, client_assertion: None, client_assertion_type: None, @@ -159,7 +158,7 @@ impl ClientCredentials { .basic_auth(username, Some(password)) .form(&RequestWithClientCredentials { body: form, - client_id, + client_id: None, client_secret: None, client_assertion: None, client_assertion_type: None, @@ -171,7 +170,7 @@ impl ClientCredentials { client_secret, } => request.form(&RequestWithClientCredentials { body: form, - client_id, + client_id: Some(client_id), client_secret: Some(client_secret), client_assertion: None, client_assertion_type: None, @@ -195,7 +194,7 @@ impl ClientCredentials { request.form(&RequestWithClientCredentials { body: form, - client_id, + client_id: None, client_secret: None, client_assertion: Some(jwt.as_str()), client_assertion_type: Some(JwtBearerClientAssertionType), @@ -228,7 +227,7 @@ impl ClientCredentials { request.form(&RequestWithClientCredentials { body: form, - client_id, + client_id: None, client_secret: None, client_assertion: Some(client_assertion.as_str()), client_assertion_type: Some(JwtBearerClientAssertionType), @@ -260,7 +259,7 @@ impl ClientCredentials { request.form(&RequestWithClientCredentials { body: form, - client_id, + client_id: Some(client_id), client_secret: Some(client_secret.as_str()), client_assertion: None, client_assertion_type: None, @@ -359,7 +358,8 @@ struct RequestWithClientCredentials<'a, T> { #[serde(flatten)] body: T, - client_id: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + client_id: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] client_secret: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/oidc-client/src/types/mod.rs b/crates/oidc-client/src/types/mod.rs index 9f4e79f8c..79fe33a1c 100644 --- a/crates/oidc-client/src/types/mod.rs +++ b/crates/oidc-client/src/types/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! OAuth 2.0 and OpenID Connect types. diff --git a/crates/oidc-client/tests/it/main.rs b/crates/oidc-client/tests/it/main.rs index 270ab8a20..ce595661d 100644 --- a/crates/oidc-client/tests/it/main.rs +++ b/crates/oidc-client/tests/it/main.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/oidc-client/tests/it/requests/authorization_code.rs b/crates/oidc-client/tests/it/requests/authorization_code.rs index d0eb2a8c1..cc3f5b210 100644 --- a/crates/oidc-client/tests/it/requests/authorization_code.rs +++ b/crates/oidc-client/tests/it/requests/authorization_code.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, num::NonZeroU32}; diff --git a/crates/oidc-client/tests/it/requests/client_credentials.rs b/crates/oidc-client/tests/it/requests/client_credentials.rs index a6da9e0ff..00b3c774f 100644 --- a/crates/oidc-client/tests/it/requests/client_credentials.rs +++ b/crates/oidc-client/tests/it/requests/client_credentials.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/oidc-client/tests/it/requests/discovery.rs b/crates/oidc-client/tests/it/requests/discovery.rs index 4b2c9d18c..cacdf7d2e 100644 --- a/crates/oidc-client/tests/it/requests/discovery.rs +++ b/crates/oidc-client/tests/it/requests/discovery.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use assert_matches::assert_matches; use mas_iana::oauth::{OAuthAuthorizationEndpointResponseType, PkceCodeChallengeMethod}; diff --git a/crates/oidc-client/tests/it/requests/jose.rs b/crates/oidc-client/tests/it/requests/jose.rs index d45358f28..dcca40252 100644 --- a/crates/oidc-client/tests/it/requests/jose.rs +++ b/crates/oidc-client/tests/it/requests/jose.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/oidc-client/tests/it/requests/mod.rs b/crates/oidc-client/tests/it/requests/mod.rs index ee2bde285..fce40a2e0 100644 --- a/crates/oidc-client/tests/it/requests/mod.rs +++ b/crates/oidc-client/tests/it/requests/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod authorization_code; mod client_credentials; diff --git a/crates/oidc-client/tests/it/requests/refresh_token.rs b/crates/oidc-client/tests/it/requests/refresh_token.rs index ebda10a10..9b6e7c390 100644 --- a/crates/oidc-client/tests/it/requests/refresh_token.rs +++ b/crates/oidc-client/tests/it/requests/refresh_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; diff --git a/crates/oidc-client/tests/it/requests/userinfo.rs b/crates/oidc-client/tests/it/requests/userinfo.rs index 2b3f9cfbd..f7d979ab0 100644 --- a/crates/oidc-client/tests/it/requests/userinfo.rs +++ b/crates/oidc-client/tests/it/requests/userinfo.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_oidc_client::requests::userinfo::fetch_userinfo; use serde_json::json; diff --git a/crates/oidc-client/tests/it/types/client_credentials.rs b/crates/oidc-client/tests/it/types/client_credentials.rs index b1939f40c..c53a98e01 100644 --- a/crates/oidc-client/tests/it/types/client_credentials.rs +++ b/crates/oidc-client/tests/it/types/client_credentials.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap; @@ -188,12 +188,8 @@ async fn pass_client_secret_jwt() { .and(move |req: &Request| { let query_pairs = form_urlencoded::parse(&req.body).collect::>(); - if query_pairs - .get("client_id") - .filter(|s| *s == CLIENT_ID) - .is_none() - { - println!("Wrong or missing client ID"); + if query_pairs.contains_key("client_id") { + println!("`client_secret_jwt` client authentication should not use `client_id`"); return false; } if query_pairs @@ -271,12 +267,8 @@ async fn pass_private_key_jwt() { .and(move |req: &Request| { let query_pairs = form_urlencoded::parse(&req.body).collect::>(); - if query_pairs - .get("client_id") - .filter(|s| *s == CLIENT_ID) - .is_none() - { - println!("Wrong or missing client ID"); + if query_pairs.contains_key("client_id") { + println!("`private_key_jwt` client authentication should not use `client_id`"); return false; } if query_pairs diff --git a/crates/oidc-client/tests/it/types/mod.rs b/crates/oidc-client/tests/it/types/mod.rs index a45ac15bd..a09d66a7b 100644 --- a/crates/oidc-client/tests/it/types/mod.rs +++ b/crates/oidc-client/tests/it/types/mod.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 Kévin Commaille. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod client_credentials; diff --git a/crates/policy/Cargo.toml b/crates/policy/Cargo.toml index ca927bcc6..7496f2726 100644 --- a/crates/policy/Cargo.toml +++ b/crates/policy/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-policy" version.workspace = true diff --git a/crates/policy/src/bin/schema.rs b/crates/policy/src/bin/schema.rs index 3fbe09adb..cc908d4c9 100644 --- a/crates/policy/src/bin/schema.rs +++ b/crates/policy/src/bin/schema.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![expect( clippy::disallowed_types, @@ -19,7 +19,7 @@ use schemars::{JsonSchema, r#gen::SchemaSettings}; fn write_schema(out_dir: Option<&Path>, file: &str) { let mut writer: Box = if let Some(out_dir) = out_dir { let path = out_dir.join(file); - eprintln!("Writing to {path:?}"); + eprintln!("Writing to {}", path.display()); let file = std::fs::File::create(path).expect("Failed to create file"); Box::new(std::io::BufWriter::new(file)) } else { diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index 5a714e9a2..b45da09ac 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub mod model; @@ -197,6 +197,11 @@ pub struct PolicyFactory { } impl PolicyFactory { + /// Load the policy from the given data source. + /// + /// # Errors + /// + /// Returns an error if the policy can't be loaded or instantiated. #[tracing::instrument(name = "policy.load", skip(source))] pub async fn load( mut source: impl AsyncRead + std::marker::Unpin, @@ -283,6 +288,12 @@ impl PolicyFactory { Ok(true) } + /// Create a new policy instance. + /// + /// # Errors + /// + /// Returns an error if the policy can't be instantiated with the current + /// dynamic data. #[tracing::instrument(name = "policy.instantiate", skip_all)] pub async fn instantiate(&self) -> Result { let data = self.dynamic_data.load(); @@ -336,6 +347,11 @@ pub enum EvaluationError { } impl Policy { + /// Evaluate the 'email' entrypoint. + /// + /// # Errors + /// + /// Returns an error if the policy engine fails to evaluate the entrypoint. #[tracing::instrument( name = "policy.evaluate_email", skip_all, @@ -355,6 +371,11 @@ impl Policy { Ok(res) } + /// Evaluate the 'register' entrypoint. + /// + /// # Errors + /// + /// Returns an error if the policy engine fails to evaluate the entrypoint. #[tracing::instrument( name = "policy.evaluate.register", skip_all, @@ -376,6 +397,11 @@ impl Policy { Ok(res) } + /// Evaluate the 'client_registration' entrypoint. + /// + /// # Errors + /// + /// Returns an error if the policy engine fails to evaluate the entrypoint. #[tracing::instrument(skip(self))] pub async fn evaluate_client_registration( &mut self, @@ -393,6 +419,11 @@ impl Policy { Ok(res) } + /// Evaluate the 'authorization_grant' entrypoint. + /// + /// # Errors + /// + /// Returns an error if the policy engine fails to evaluate the entrypoint. #[tracing::instrument( name = "policy.evaluate.authorization_grant", skip_all, diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index d57a81655..2f54ae8bb 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Input and output types for policy evaluation. //! diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b31302fc0..07cd3a913 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-router" version.workspace = true diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs index 896f17a52..3440f8bc6 100644 --- a/crates/router/src/endpoints.rs +++ b/crates/router/src/endpoints.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use serde::{Deserialize, Serialize}; use ulid::Ulid; @@ -738,6 +738,29 @@ impl Route for UpstreamOAuth2Link { } } +/// `POST /upstream/backchannel-logout/{id}` +pub struct UpstreamOAuth2BackchannelLogout { + id: Ulid, +} + +impl UpstreamOAuth2BackchannelLogout { + #[must_use] + pub const fn new(id: Ulid) -> Self { + Self { id } + } +} + +impl Route for UpstreamOAuth2BackchannelLogout { + type Query = (); + fn route() -> &'static str { + "/upstream/backchannel-logout/{provider_id}" + } + + fn path(&self) -> std::borrow::Cow<'static, str> { + format!("/upstream/backchannel-logout/{}", self.id).into() + } +} + /// `GET|POST /link` #[derive(Default, Serialize, Deserialize, Debug, Clone)] pub struct DeviceCodeLink { diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 5a06f1891..0308fdfcd 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. pub(crate) mod endpoints; pub(crate) mod traits; diff --git a/crates/router/src/traits.rs b/crates/router/src/traits.rs index 53bb40678..5e7f13bdb 100644 --- a/crates/router/src/traits.rs +++ b/crates/router/src/traits.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::borrow::Cow; diff --git a/crates/router/src/url_builder.rs b/crates/router/src/url_builder.rs index d3a2f6f64..f216fb343 100644 --- a/crates/router/src/url_builder.rs +++ b/crates/router/src/url_builder.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Utility to build URLs diff --git a/crates/spa/Cargo.toml b/crates/spa/Cargo.toml index 292338a67..5287abf15 100644 --- a/crates/spa/Cargo.toml +++ b/crates/spa/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-spa" version.workspace = true diff --git a/crates/spa/src/lib.rs b/crates/spa/src/lib.rs index 91fc183d0..af3be8def 100644 --- a/crates/spa/src/lib.rs +++ b/crates/spa/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(rustdoc::missing_crate_level_docs)] diff --git a/crates/spa/src/vite.rs b/crates/spa/src/vite.rs index 31d5d3c7e..e2706d512 100644 --- a/crates/spa/src/vite.rs +++ b/crates/spa/src/vite.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::{BTreeSet, HashMap}; diff --git a/crates/storage-pg/.sqlx/query-0f2ea548e00b080502edc04ee97ea304d43c336ce80723789ff3e66c0dd4d86c.json b/crates/storage-pg/.sqlx/query-0f2ea548e00b080502edc04ee97ea304d43c336ce80723789ff3e66c0dd4d86c.json new file mode 100644 index 000000000..1eb87fd3f --- /dev/null +++ b/crates/storage-pg/.sqlx/query-0f2ea548e00b080502edc04ee97ea304d43c336ce80723789ff3e66c0dd4d86c.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters,\n forward_login_hint,\n ui_order,\n on_backchannel_logout,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25)\n ON CONFLICT (upstream_oauth_provider_id)\n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n id_token_signed_response_alg = EXCLUDED.id_token_signed_response_alg,\n fetch_userinfo = EXCLUDED.fetch_userinfo,\n userinfo_signed_response_alg = EXCLUDED.userinfo_signed_response_alg,\n disabled_at = NULL,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n userinfo_endpoint_override = EXCLUDED.userinfo_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode,\n response_mode = EXCLUDED.response_mode,\n additional_parameters = EXCLUDED.additional_parameters,\n forward_login_hint = EXCLUDED.forward_login_hint,\n ui_order = EXCLUDED.ui_order,\n on_backchannel_logout = EXCLUDED.on_backchannel_logout\n RETURNING created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Jsonb", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Jsonb", + "Bool", + "Int4", + "Text", + "Timestamptz" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0f2ea548e00b080502edc04ee97ea304d43c336ce80723789ff3e66c0dd4d86c" +} diff --git a/crates/storage-pg/.sqlx/query-a711f4c6fa38b98c960ee565038d42ea16db436352b19fcd3b2c620c73d9cc0c.json b/crates/storage-pg/.sqlx/query-3312f901f70c3b69e0d315206c31ffe11da64835ae297c9277271b8971d5de81.json similarity index 78% rename from crates/storage-pg/.sqlx/query-a711f4c6fa38b98c960ee565038d42ea16db436352b19fcd3b2c620c73d9cc0c.json rename to crates/storage-pg/.sqlx/query-3312f901f70c3b69e0d315206c31ffe11da64835ae297c9277271b8971d5de81.json index 9944e855b..3f837630f 100644 --- a/crates/storage-pg/.sqlx/query-a711f4c6fa38b98c960ee565038d42ea16db436352b19fcd3b2c620c73d9cc0c.json +++ b/crates/storage-pg/.sqlx/query-3312f901f70c3b69e0d315206c31ffe11da64835ae297c9277271b8971d5de81.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n forward_login_hint,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)\n ", + "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n forward_login_hint,\n on_backchannel_logout,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,\n $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23)\n ", "describe": { "columns": [], "parameters": { @@ -26,10 +26,11 @@ "Text", "Text", "Bool", + "Text", "Timestamptz" ] }, "nullable": [] }, - "hash": "a711f4c6fa38b98c960ee565038d42ea16db436352b19fcd3b2c620c73d9cc0c" + "hash": "3312f901f70c3b69e0d315206c31ffe11da64835ae297c9277271b8971d5de81" } diff --git a/crates/storage-pg/.sqlx/query-585a1e78834c953c80a0af9215348b0f551b16f4cb57c022b50212cfc3d8431f.json b/crates/storage-pg/.sqlx/query-585a1e78834c953c80a0af9215348b0f551b16f4cb57c022b50212cfc3d8431f.json deleted file mode 100644 index a7b63ca21..000000000 --- a/crates/storage-pg/.sqlx/query-585a1e78834c953c80a0af9215348b0f551b16f4cb57c022b50212cfc3d8431f.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters,\n forward_login_hint,\n ui_order,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,\n $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24)\n ON CONFLICT (upstream_oauth_provider_id)\n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n id_token_signed_response_alg = EXCLUDED.id_token_signed_response_alg,\n fetch_userinfo = EXCLUDED.fetch_userinfo,\n userinfo_signed_response_alg = EXCLUDED.userinfo_signed_response_alg,\n disabled_at = NULL,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n userinfo_endpoint_override = EXCLUDED.userinfo_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode,\n response_mode = EXCLUDED.response_mode,\n additional_parameters = EXCLUDED.additional_parameters,\n forward_login_hint = EXCLUDED.forward_login_hint,\n ui_order = EXCLUDED.ui_order\n RETURNING created_at\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Bool", - "Text", - "Text", - "Text", - "Jsonb", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Jsonb", - "Bool", - "Int4", - "Timestamptz" - ] - }, - "nullable": [ - false - ] - }, - "hash": "585a1e78834c953c80a0af9215348b0f551b16f4cb57c022b50212cfc3d8431f" -} diff --git a/crates/storage-pg/.sqlx/query-f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec.json b/crates/storage-pg/.sqlx/query-5eea2f4c3e82ae606b09b8a81332594c97ba0afe972f0fee145b6094789fb6c7.json similarity index 85% rename from crates/storage-pg/.sqlx/query-f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec.json rename to crates/storage-pg/.sqlx/query-5eea2f4c3e82ae606b09b8a81332594c97ba0afe972f0fee145b6094789fb6c7.json index cc7f8d1af..3d81b9141 100644 --- a/crates/storage-pg/.sqlx/query-f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec.json +++ b/crates/storage-pg/.sqlx/query-5eea2f4c3e82ae606b09b8a81332594c97ba0afe972f0fee145b6094789fb6c7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n WHERE email = $1\n ", + "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n WHERE LOWER(email) = LOWER($1)\n ", "describe": { "columns": [ { @@ -36,5 +36,5 @@ false ] }, - "hash": "f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec" + "hash": "5eea2f4c3e82ae606b09b8a81332594c97ba0afe972f0fee145b6094789fb6c7" } diff --git a/crates/storage-pg/.sqlx/query-5f5245ace61b896f92be78ab4fef701b37c9e3c2f4a332f418b9fb2625a0fe3f.json b/crates/storage-pg/.sqlx/query-5f5245ace61b896f92be78ab4fef701b37c9e3c2f4a332f418b9fb2625a0fe3f.json deleted file mode 100644 index c33da04d8..000000000 --- a/crates/storage-pg/.sqlx/query-5f5245ace61b896f92be78ab4fef701b37c9e3c2f4a332f418b9fb2625a0fe3f.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE upstream_oauth_authorization_sessions\n SET upstream_oauth_link_id = $1,\n completed_at = $2,\n id_token = $3,\n extra_callback_parameters = $4,\n userinfo = $5\n WHERE upstream_oauth_authorization_session_id = $6\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Timestamptz", - "Text", - "Jsonb", - "Jsonb", - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "5f5245ace61b896f92be78ab4fef701b37c9e3c2f4a332f418b9fb2625a0fe3f" -} diff --git a/crates/storage-pg/.sqlx/query-a82b87ccfaa1de9a8e6433aaa67382fbb5029d5f7adf95aaa0decd668d25ba89.json b/crates/storage-pg/.sqlx/query-6589987e88fa9dbbd2bd48acd910e08bab57721007c64ef2597cb09a62100792.json similarity index 91% rename from crates/storage-pg/.sqlx/query-a82b87ccfaa1de9a8e6433aaa67382fbb5029d5f7adf95aaa0decd668d25ba89.json rename to crates/storage-pg/.sqlx/query-6589987e88fa9dbbd2bd48acd910e08bab57721007c64ef2597cb09a62100792.json index 7c1a26a86..6bd2768cc 100644 --- a/crates/storage-pg/.sqlx/query-a82b87ccfaa1de9a8e6433aaa67382fbb5029d5f7adf95aaa0decd668d25ba89.json +++ b/crates/storage-pg/.sqlx/query-6589987e88fa9dbbd2bd48acd910e08bab57721007c64ef2597cb09a62100792.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json>\",\n forward_login_hint\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n ", + "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json>\",\n forward_login_hint,\n on_backchannel_logout\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n ", "describe": { "columns": [ { @@ -122,6 +122,11 @@ "ordinal": 23, "name": "forward_login_hint", "type_info": "Bool" + }, + { + "ordinal": 24, + "name": "on_backchannel_logout", + "type_info": "Text" } ], "parameters": { @@ -153,8 +158,9 @@ false, true, true, + false, false ] }, - "hash": "a82b87ccfaa1de9a8e6433aaa67382fbb5029d5f7adf95aaa0decd668d25ba89" + "hash": "6589987e88fa9dbbd2bd48acd910e08bab57721007c64ef2597cb09a62100792" } diff --git a/crates/storage-pg/.sqlx/query-98a5491eb5f10997ac1f3718c835903ac99d9bb8ca4d79c908b25a6d1209b9b1.json b/crates/storage-pg/.sqlx/query-98a5491eb5f10997ac1f3718c835903ac99d9bb8ca4d79c908b25a6d1209b9b1.json new file mode 100644 index 000000000..75f013b53 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-98a5491eb5f10997ac1f3718c835903ac99d9bb8ca4d79c908b25a6d1209b9b1.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE users\n SET deactivated_at = NULL\n WHERE user_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "98a5491eb5f10997ac1f3718c835903ac99d9bb8ca4d79c908b25a6d1209b9b1" +} diff --git a/crates/storage-pg/.sqlx/query-e6d66a7980933c12ab046958e02d419129ef52ac45bea4345471838016cae917.json b/crates/storage-pg/.sqlx/query-99394fbd9c07d6d24429934b3f7344dfab024b42e47ddc7bd9e551897ba6e9b8.json similarity index 89% rename from crates/storage-pg/.sqlx/query-e6d66a7980933c12ab046958e02d419129ef52ac45bea4345471838016cae917.json rename to crates/storage-pg/.sqlx/query-99394fbd9c07d6d24429934b3f7344dfab024b42e47ddc7bd9e551897ba6e9b8.json index d544590c4..eb1a801c4 100644 --- a/crates/storage-pg/.sqlx/query-e6d66a7980933c12ab046958e02d419129ef52ac45bea4345471838016cae917.json +++ b/crates/storage-pg/.sqlx/query-99394fbd9c07d6d24429934b3f7344dfab024b42e47ddc7bd9e551897ba6e9b8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json>\",\n forward_login_hint\n FROM upstream_oauth_providers\n WHERE disabled_at IS NULL\n ORDER BY ui_order ASC, upstream_oauth_provider_id ASC\n ", + "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n id_token_signed_response_alg,\n fetch_userinfo,\n userinfo_signed_response_alg,\n created_at,\n disabled_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n userinfo_endpoint_override,\n discovery_mode,\n pkce_mode,\n response_mode,\n additional_parameters as \"additional_parameters: Json>\",\n forward_login_hint,\n on_backchannel_logout\n FROM upstream_oauth_providers\n WHERE disabled_at IS NULL\n ORDER BY ui_order ASC, upstream_oauth_provider_id ASC\n ", "describe": { "columns": [ { @@ -122,6 +122,11 @@ "ordinal": 23, "name": "forward_login_hint", "type_info": "Bool" + }, + { + "ordinal": 24, + "name": "on_backchannel_logout", + "type_info": "Text" } ], "parameters": { @@ -151,8 +156,9 @@ false, true, true, + false, false ] }, - "hash": "e6d66a7980933c12ab046958e02d419129ef52ac45bea4345471838016cae917" + "hash": "99394fbd9c07d6d24429934b3f7344dfab024b42e47ddc7bd9e551897ba6e9b8" } diff --git a/crates/storage-pg/.sqlx/query-f7d26de1d380e3e52f47f2b89ed7506e1e4cca72682bc7737e6508dc4015b8d5.json b/crates/storage-pg/.sqlx/query-ca093cab5143bb3dded2eda9e82473215f4d3c549ea2c5a4f860a102cc46a667.json similarity index 84% rename from crates/storage-pg/.sqlx/query-f7d26de1d380e3e52f47f2b89ed7506e1e4cca72682bc7737e6508dc4015b8d5.json rename to crates/storage-pg/.sqlx/query-ca093cab5143bb3dded2eda9e82473215f4d3c549ea2c5a4f860a102cc46a667.json index 3e278f223..8ae291bb6 100644 --- a/crates/storage-pg/.sqlx/query-f7d26de1d380e3e52f47f2b89ed7506e1e4cca72682bc7737e6508dc4015b8d5.json +++ b/crates/storage-pg/.sqlx/query-ca093cab5143bb3dded2eda9e82473215f4d3c549ea2c5a4f860a102cc46a667.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n\n WHERE user_id = $1 AND email = $2\n ", + "query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n\n WHERE user_id = $1 AND LOWER(email) = LOWER($2)\n ", "describe": { "columns": [ { @@ -37,5 +37,5 @@ false ] }, - "hash": "f7d26de1d380e3e52f47f2b89ed7506e1e4cca72682bc7737e6508dc4015b8d5" + "hash": "ca093cab5143bb3dded2eda9e82473215f4d3c549ea2c5a4f860a102cc46a667" } diff --git a/crates/storage-pg/.sqlx/query-37a124678323380357fa9d1375fd125fb35476ac3008e5adbd04a761d5edcd42.json b/crates/storage-pg/.sqlx/query-e62d043f86e7232e6e9433631f8273e7ed0770c81071cf1f17516d3a45881ae9.json similarity index 77% rename from crates/storage-pg/.sqlx/query-37a124678323380357fa9d1375fd125fb35476ac3008e5adbd04a761d5edcd42.json rename to crates/storage-pg/.sqlx/query-e62d043f86e7232e6e9433631f8273e7ed0770c81071cf1f17516d3a45881ae9.json index 0e28ac022..c3c2e2507 100644 --- a/crates/storage-pg/.sqlx/query-37a124678323380357fa9d1375fd125fb35476ac3008e5adbd04a761d5edcd42.json +++ b/crates/storage-pg/.sqlx/query-e62d043f86e7232e6e9433631f8273e7ed0770c81071cf1f17516d3a45881ae9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n upstream_oauth_authorization_session_id,\n upstream_oauth_provider_id,\n upstream_oauth_link_id,\n state,\n code_challenge_verifier,\n nonce,\n id_token,\n extra_callback_parameters,\n userinfo,\n created_at,\n completed_at,\n consumed_at,\n unlinked_at\n FROM upstream_oauth_authorization_sessions\n WHERE upstream_oauth_authorization_session_id = $1\n ", + "query": "\n SELECT\n upstream_oauth_authorization_session_id,\n upstream_oauth_provider_id,\n upstream_oauth_link_id,\n state,\n code_challenge_verifier,\n nonce,\n id_token,\n id_token_claims,\n extra_callback_parameters,\n userinfo,\n created_at,\n completed_at,\n consumed_at,\n unlinked_at\n FROM upstream_oauth_authorization_sessions\n WHERE upstream_oauth_authorization_session_id = $1\n ", "describe": { "columns": [ { @@ -40,31 +40,36 @@ }, { "ordinal": 7, - "name": "extra_callback_parameters", + "name": "id_token_claims", "type_info": "Jsonb" }, { "ordinal": 8, - "name": "userinfo", + "name": "extra_callback_parameters", "type_info": "Jsonb" }, { "ordinal": 9, + "name": "userinfo", + "type_info": "Jsonb" + }, + { + "ordinal": 10, "name": "created_at", "type_info": "Timestamptz" }, { - "ordinal": 10, + "ordinal": 11, "name": "completed_at", "type_info": "Timestamptz" }, { - "ordinal": 11, + "ordinal": 12, "name": "consumed_at", "type_info": "Timestamptz" }, { - "ordinal": 12, + "ordinal": 13, "name": "unlinked_at", "type_info": "Timestamptz" } @@ -84,11 +89,12 @@ true, true, true, + true, false, true, true, true ] }, - "hash": "37a124678323380357fa9d1375fd125fb35476ac3008e5adbd04a761d5edcd42" + "hash": "e62d043f86e7232e6e9433631f8273e7ed0770c81071cf1f17516d3a45881ae9" } diff --git a/crates/storage-pg/.sqlx/query-fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50.json b/crates/storage-pg/.sqlx/query-fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50.json new file mode 100644 index 000000000..072e6f57b --- /dev/null +++ b/crates/storage-pg/.sqlx/query-fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE upstream_oauth_authorization_sessions\n SET upstream_oauth_link_id = $1\n , completed_at = $2\n , id_token = $3\n , id_token_claims = $4\n , extra_callback_parameters = $5\n , userinfo = $6\n WHERE upstream_oauth_authorization_session_id = $7\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz", + "Text", + "Jsonb", + "Jsonb", + "Jsonb", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50" +} diff --git a/crates/storage-pg/Cargo.toml b/crates/storage-pg/Cargo.toml index 0bf34ec0b..149e92fc6 100644 --- a/crates/storage-pg/Cargo.toml +++ b/crates/storage-pg/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-storage-pg" version.workspace = true diff --git a/crates/storage-pg/build.rs b/crates/storage-pg/build.rs index d13fb1c76..007c4622f 100644 --- a/crates/storage-pg/build.rs +++ b/crates/storage-pg/build.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. fn main() { // trigger recompilation when a new migration is added diff --git a/crates/storage-pg/migrations/20250602212102_upstream_oauth2_id_token_claims.sql b/crates/storage-pg/migrations/20250602212102_upstream_oauth2_id_token_claims.sql new file mode 100644 index 000000000..6cb78a4c2 --- /dev/null +++ b/crates/storage-pg/migrations/20250602212102_upstream_oauth2_id_token_claims.sql @@ -0,0 +1,8 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- This is the decoded claims from the ID token stored as JSONB +ALTER TABLE upstream_oauth_authorization_sessions + ADD COLUMN id_token_claims JSONB; diff --git a/crates/storage-pg/migrations/20250602212103_upstream_oauth2_id_token_claims_sub_sid_index.sql b/crates/storage-pg/migrations/20250602212103_upstream_oauth2_id_token_claims_sub_sid_index.sql new file mode 100644 index 000000000..327022168 --- /dev/null +++ b/crates/storage-pg/migrations/20250602212103_upstream_oauth2_id_token_claims_sub_sid_index.sql @@ -0,0 +1,15 @@ +-- no-transaction +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- We'll be requesting authorization sessions by provider, sub and sid, so we'll +-- need to index those columns +CREATE INDEX CONCURRENTLY IF NOT EXISTS + upstream_oauth_authorization_sessions_sub_sid_idx + ON upstream_oauth_authorization_sessions ( + upstream_oauth_provider_id, + (id_token_claims->>'sub'), + (id_token_claims->>'sid') + ); diff --git a/crates/storage-pg/migrations/20250602212104_upstream_oauth2_id_token_claims_sid_sub_index.sql b/crates/storage-pg/migrations/20250602212104_upstream_oauth2_id_token_claims_sid_sub_index.sql new file mode 100644 index 000000000..097c3da32 --- /dev/null +++ b/crates/storage-pg/migrations/20250602212104_upstream_oauth2_id_token_claims_sid_sub_index.sql @@ -0,0 +1,15 @@ +-- no-transaction +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- We'll be requesting authorization sessions by provider, sub and sid, so we'll +-- need to index those columns +CREATE INDEX CONCURRENTLY IF NOT EXISTS + upstream_oauth_authorization_sessions_sid_sub_idx + ON upstream_oauth_authorization_sessions ( + upstream_oauth_provider_id, + (id_token_claims->>'sid'), + (id_token_claims->>'sub') + ); diff --git a/crates/storage-pg/migrations/20250630120643_upstream_oauth_on_backchannel_logout.sql b/crates/storage-pg/migrations/20250630120643_upstream_oauth_on_backchannel_logout.sql new file mode 100644 index 000000000..f6031ca62 --- /dev/null +++ b/crates/storage-pg/migrations/20250630120643_upstream_oauth_on_backchannel_logout.sql @@ -0,0 +1,10 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- This defines the behavior when receiving a backchannel logout notification +ALTER TABLE "upstream_oauth_providers" + ADD COLUMN "on_backchannel_logout" TEXT + NOT NULL + DEFAULT 'do_nothing'; diff --git a/crates/storage-pg/migrations/20250708155857_idx_user_emails_lower_email.sql b/crates/storage-pg/migrations/20250708155857_idx_user_emails_lower_email.sql new file mode 100644 index 000000000..06b3dde6a --- /dev/null +++ b/crates/storage-pg/migrations/20250708155857_idx_user_emails_lower_email.sql @@ -0,0 +1,11 @@ +-- no-transaction +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- When we're looking up an email address, we want to be able to do a case-insensitive +-- lookup, so we index the email address lowercase and request it like that +CREATE INDEX CONCURRENTLY + user_emails_lower_email_idx + ON user_emails (LOWER(email)); diff --git a/crates/storage-pg/migrations/20250709142230_id_token_claims_trigger.sql b/crates/storage-pg/migrations/20250709142230_id_token_claims_trigger.sql new file mode 100644 index 000000000..32d304721 --- /dev/null +++ b/crates/storage-pg/migrations/20250709142230_id_token_claims_trigger.sql @@ -0,0 +1,51 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- We may be running an older version of the app that doesn't fill in the +-- id_token_claims column when the id_token column is populated. So we add a +-- trigger to fill in the id_token_claims column if it's NULL. +-- +-- We will be able to remove this trigger in a future version of the app. +-- +-- We backfill in a second migration after this one to make sure we don't miss +-- any rows, and don't lock the table for too long. +CREATE OR REPLACE FUNCTION fill_id_token_claims() +RETURNS TRIGGER AS $$ +BEGIN + -- Only process if id_token_claims is NULL but id_token is not NULL + IF NEW.id_token_claims IS NULL AND NEW.id_token IS NOT NULL AND NEW.id_token != '' THEN + BEGIN + -- Decode JWT payload inline + NEW.id_token_claims := ( + CASE + WHEN split_part(NEW.id_token, '.', 2) = '' THEN NULL + ELSE + (convert_from( + decode( + replace(replace(split_part(NEW.id_token, '.', 2), '-', '+'), '_', '/') || + repeat('=', (4 - length(split_part(NEW.id_token, '.', 2)) % 4) % 4), + 'base64' + ), + 'UTF8' + ))::JSONB + END + ); + EXCEPTION + WHEN OTHERS THEN + -- If JWT decoding fails, leave id_token_claims as NULL + NEW.id_token_claims := NULL; + END; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +CREATE TRIGGER trg_fill_id_token_claims + BEFORE INSERT OR UPDATE ON upstream_oauth_authorization_sessions + FOR EACH ROW + WHEN (NEW.id_token_claims IS NULL AND NEW.id_token IS NOT NULL AND NEW.id_token <> '') + EXECUTE FUNCTION fill_id_token_claims(); diff --git a/crates/storage-pg/migrations/20250709142240_backfill_id_token_claims.sql b/crates/storage-pg/migrations/20250709142240_backfill_id_token_claims.sql new file mode 100644 index 000000000..c2fa067af --- /dev/null +++ b/crates/storage-pg/migrations/20250709142240_backfill_id_token_claims.sql @@ -0,0 +1,22 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE in the repository root for full details. + +-- This backfills the id_token_claims column in the upstream_oauth_authorization_sessions table +-- by decoding the id_token column and storing the decoded claims in the id_token_claims column. +UPDATE upstream_oauth_authorization_sessions +SET id_token_claims = CASE + WHEN id_token IS NULL OR id_token = '' THEN NULL + WHEN split_part(id_token, '.', 2) = '' THEN NULL + ELSE + (convert_from( + decode( + replace(replace(split_part(id_token, '.', 2), '-', '+'), '_', '/') || + repeat('=', (4 - length(split_part(id_token, '.', 2)) % 4) % 4), + 'base64' + ), + 'UTF8' + ))::JSONB +END +WHERE id_token IS NOT NULL AND id_token_claims IS NULL; diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index 4424406f2..bfa0b66ba 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing PostgreSQL implementation of repositories for sessions @@ -82,7 +82,6 @@ use priv_::{AppSessionLookup, AppSessionLookupIden}; impl TryFrom for AppSession { type Error = DatabaseError; - #[allow(clippy::too_many_lines)] fn try_from(value: AppSessionLookup) -> Result { // This is annoying to do, but we have to match on all the fields to determine // whether it's a compat session or an oauth2 session @@ -257,7 +256,6 @@ fn split_filter( impl AppSessionRepository for PgAppSessionRepository<'_> { type Error = DatabaseError; - #[allow(clippy::too_many_lines)] #[tracing::instrument( name = "db.app_session.list", fields( diff --git a/crates/storage-pg/src/compat/access_token.rs b/crates/storage-pg/src/compat/access_token.rs index 852b0e934..2ca36fc9a 100644 --- a/crates/storage-pg/src/compat/access_token.rs +++ b/crates/storage-pg/src/compat/access_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; diff --git a/crates/storage-pg/src/compat/mod.rs b/crates/storage-pg/src/compat/mod.rs index 60332fd50..32f26303f 100644 --- a/crates/storage-pg/src/compat/mod.rs +++ b/crates/storage-pg/src/compat/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing PostgreSQL implementation of repositories for the //! compatibility layer diff --git a/crates/storage-pg/src/compat/refresh_token.rs b/crates/storage-pg/src/compat/refresh_token.rs index 41188e010..67a2b839e 100644 --- a/crates/storage-pg/src/compat/refresh_token.rs +++ b/crates/storage-pg/src/compat/refresh_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/crates/storage-pg/src/compat/session.rs b/crates/storage-pg/src/compat/session.rs index 31f012477..d5d41fb7b 100644 --- a/crates/storage-pg/src/compat/session.rs +++ b/crates/storage-pg/src/compat/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; @@ -27,7 +27,7 @@ use uuid::Uuid; use crate::{ DatabaseError, DatabaseInconsistencyError, filter::{Filter, StatementExt, StatementWithJoinsExt}, - iden::{CompatSessions, CompatSsoLogins}, + iden::{CompatSessions, CompatSsoLogins, UserSessions}, pagination::QueryBuilderExt, tracing::ExecuteExt, }; @@ -190,6 +190,18 @@ impl Filter for CompatSessionFilter<'_> { Expr::col((CompatSessions::Table, CompatSessions::UserSessionId)) .eq(Uuid::from(browser_session.id)) })) + .add_option(self.browser_session_filter().map(|browser_session_filter| { + Expr::col((CompatSessions::Table, CompatSessions::UserSessionId)).in_subquery( + Query::select() + .expr(Expr::col(( + UserSessions::Table, + UserSessions::UserSessionId, + ))) + .apply_filter(browser_session_filter) + .from(UserSessions::Table) + .take(), + ) + })) .add_option(self.state().map(|state| { if state.is_active() { Expr::col((CompatSessions::Table, CompatSessions::FinishedAt)).is_null() diff --git a/crates/storage-pg/src/compat/sso_login.rs b/crates/storage-pg/src/compat/sso_login.rs index 2c794921b..822812ae5 100644 --- a/crates/storage-pg/src/compat/sso_login.rs +++ b/crates/storage-pg/src/compat/sso_login.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/crates/storage-pg/src/errors.rs b/crates/storage-pg/src/errors.rs index 80fb9fa66..4c50557ff 100644 --- a/crates/storage-pg/src/errors.rs +++ b/crates/storage-pg/src/errors.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use sqlx::postgres::PgQueryResult; use thiserror::Error; diff --git a/crates/storage-pg/src/filter.rs b/crates/storage-pg/src/filter.rs index e0771fc4e..d8bf3e930 100644 --- a/crates/storage-pg/src/filter.rs +++ b/crates/storage-pg/src/filter.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. /// A filter which can be applied to a query pub(crate) trait Filter { diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index 6692c7a75..e6c03acc4 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Table and column identifiers used by [`sea_query`] @@ -18,6 +18,18 @@ pub enum UserSessions { LastActiveIp, } +#[derive(sea_query::Iden)] +#[expect(dead_code)] +pub enum UserSessionAuthentications { + Table, + UserSessionAuthenticationId, + UserSessionId, + CreatedAt, + UserPasswordId, + #[iden = "upstream_oauth_authorization_session_id"] + UpstreamOAuthAuthorizationSessionId, +} + #[derive(sea_query::Iden)] pub enum Users { Table, @@ -124,6 +136,7 @@ pub enum UpstreamOAuthProviders { TokenEndpointOverride, AuthorizationEndpointOverride, UserinfoEndpointOverride, + OnBackchannelLogout, } #[derive(sea_query::Iden)] @@ -140,6 +153,29 @@ pub enum UpstreamOAuthLinks { CreatedAt, } +#[derive(sea_query::Iden)] +#[iden = "upstream_oauth_authorization_sessions"] +pub enum UpstreamOAuthAuthorizationSessions { + Table, + #[iden = "upstream_oauth_authorization_session_id"] + UpstreamOAuthAuthorizationSessionId, + #[iden = "upstream_oauth_provider_id"] + UpstreamOAuthProviderId, + #[iden = "upstream_oauth_link_id"] + UpstreamOAuthLinkId, + State, + CodeChallengeVerifier, + Nonce, + IdToken, + IdTokenClaims, + ExtraCallbackParameters, + Userinfo, + CreatedAt, + CompletedAt, + ConsumedAt, + UnlinkedAt, +} + #[derive(sea_query::Iden)] pub enum UserRegistrationTokens { Table, diff --git a/crates/storage-pg/src/lib.rs b/crates/storage-pg/src/lib.rs index 30882cfa8..108248537 100644 --- a/crates/storage-pg/src/lib.rs +++ b/crates/storage-pg/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! An implementation of the storage traits for a PostgreSQL database //! diff --git a/crates/storage-pg/src/oauth2/access_token.rs b/crates/storage-pg/src/oauth2/access_token.rs index de652739f..fb5406285 100644 --- a/crates/storage-pg/src/oauth2/access_token.rs +++ b/crates/storage-pg/src/oauth2/access_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Duration, Utc}; diff --git a/crates/storage-pg/src/oauth2/authorization_grant.rs b/crates/storage-pg/src/oauth2/authorization_grant.rs index 59c5c2338..d9727ead0 100644 --- a/crates/storage-pg/src/oauth2/authorization_grant.rs +++ b/crates/storage-pg/src/oauth2/authorization_grant.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -60,7 +60,6 @@ struct GrantLookup { impl TryFrom for AuthorizationGrant { type Error = DatabaseInconsistencyError; - #[allow(clippy::too_many_lines)] fn try_from(value: GrantLookup) -> Result { let id = value.oauth2_authorization_grant_id.into(); let scope: Scope = value.scope.parse().map_err(|e| { diff --git a/crates/storage-pg/src/oauth2/client.rs b/crates/storage-pg/src/oauth2/client.rs index 60e1ebb54..2c76f484d 100644 --- a/crates/storage-pg/src/oauth2/client.rs +++ b/crates/storage-pg/src/oauth2/client.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::{BTreeMap, BTreeSet}, @@ -67,7 +67,6 @@ struct OAuth2ClientLookup { impl TryInto for OAuth2ClientLookup { type Error = DatabaseInconsistencyError; - #[allow(clippy::too_many_lines)] // TODO: refactor some of the field parsing fn try_into(self) -> Result { let id = Ulid::from(self.oauth2_client_id); @@ -416,7 +415,6 @@ impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> { ), err, )] - #[allow(clippy::too_many_lines)] async fn add( &mut self, rng: &mut (dyn RngCore + Send), diff --git a/crates/storage-pg/src/oauth2/device_code_grant.rs b/crates/storage-pg/src/oauth2/device_code_grant.rs index ebed4d859..d7b3f0977 100644 --- a/crates/storage-pg/src/oauth2/device_code_grant.rs +++ b/crates/storage-pg/src/oauth2/device_code_grant.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index 3f70fd5cc..547128d10 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementations of the OAuth2-related //! repositories diff --git a/crates/storage-pg/src/oauth2/refresh_token.rs b/crates/storage-pg/src/oauth2/refresh_token.rs index 742d3ae68..225bccaa8 100644 --- a/crates/storage-pg/src/oauth2/refresh_token.rs +++ b/crates/storage-pg/src/oauth2/refresh_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index 50cfa03ce..3ab210eda 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; @@ -27,7 +27,7 @@ use uuid::Uuid; use crate::{ DatabaseError, DatabaseInconsistencyError, filter::{Filter, StatementExt}, - iden::{OAuth2Clients, OAuth2Sessions}, + iden::{OAuth2Clients, OAuth2Sessions, UserSessions}, pagination::QueryBuilderExt, tracing::ExecuteExt, }; @@ -151,6 +151,18 @@ impl Filter for OAuth2SessionFilter<'_> { Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserSessionId)) .eq(Uuid::from(browser_session.id)) })) + .add_option(self.browser_session_filter().map(|browser_session_filter| { + Expr::col((OAuth2Sessions::Table, OAuth2Sessions::UserSessionId)).in_subquery( + Query::select() + .expr(Expr::col(( + UserSessions::Table, + UserSessions::UserSessionId, + ))) + .apply_filter(browser_session_filter) + .from(UserSessions::Table) + .take(), + ) + })) .add_option(self.state().map(|state| { if state.is_active() { Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt)).is_null() diff --git a/crates/storage-pg/src/pagination.rs b/crates/storage-pg/src/pagination.rs index 34f317a73..8e83c2372 100644 --- a/crates/storage-pg/src/pagination.rs +++ b/crates/storage-pg/src/pagination.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Utilities to manage paginated queries. diff --git a/crates/storage-pg/src/policy_data.rs b/crates/storage-pg/src/policy_data.rs index 65615b348..0ce9b15fa 100644 --- a/crates/storage-pg/src/policy_data.rs +++ b/crates/storage-pg/src/policy_data.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the policy data //! storage. diff --git a/crates/storage-pg/src/queue/job.rs b/crates/storage-pg/src/queue/job.rs index 6a7d81e01..c2208c515 100644 --- a/crates/storage-pg/src/queue/job.rs +++ b/crates/storage-pg/src/queue/job.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the //! [`QueueJobRepository`]. diff --git a/crates/storage-pg/src/queue/mod.rs b/crates/storage-pg/src/queue/mod.rs index 1c00e1d7d..d3570e59f 100644 --- a/crates/storage-pg/src/queue/mod.rs +++ b/crates/storage-pg/src/queue/mod.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the job queue diff --git a/crates/storage-pg/src/queue/schedule.rs b/crates/storage-pg/src/queue/schedule.rs index afd09a8e3..fd28ef141 100644 --- a/crates/storage-pg/src/queue/schedule.rs +++ b/crates/storage-pg/src/queue/schedule.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the //! [`QueueScheduleRepository`]. diff --git a/crates/storage-pg/src/queue/worker.rs b/crates/storage-pg/src/queue/worker.rs index 6c4a8e6c5..3aa21d5ce 100644 --- a/crates/storage-pg/src/queue/worker.rs +++ b/crates/storage-pg/src/queue/worker.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the //! [`QueueWorkerRepository`]. diff --git a/crates/storage-pg/src/repository.rs b/crates/storage-pg/src/repository.rs index 8dc02b9bb..7911cd2b6 100644 --- a/crates/storage-pg/src/repository.rs +++ b/crates/storage-pg/src/repository.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::ops::{Deref, DerefMut}; diff --git a/crates/storage-pg/src/telemetry.rs b/crates/storage-pg/src/telemetry.rs index 93c74e74f..4771f22cc 100644 --- a/crates/storage-pg/src/telemetry.rs +++ b/crates/storage-pg/src/telemetry.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/storage-pg/src/tracing.rs b/crates/storage-pg/src/tracing.rs index 137bf036c..4cab1fd79 100644 --- a/crates/storage-pg/src/tracing.rs +++ b/crates/storage-pg/src/tracing.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use opentelemetry_semantic_conventions::attribute::DB_QUERY_TEXT; use tracing::Span; diff --git a/crates/storage-pg/src/upstream_oauth2/link.rs b/crates/storage-pg/src/upstream_oauth2/link.rs index 390029f1f..4d04cb68e 100644 --- a/crates/storage-pg/src/upstream_oauth2/link.rs +++ b/crates/storage-pg/src/upstream_oauth2/link.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/crates/storage-pg/src/upstream_oauth2/mod.rs b/crates/storage-pg/src/upstream_oauth2/mod.rs index a5cda570b..0ec2c3670 100644 --- a/crates/storage-pg/src/upstream_oauth2/mod.rs +++ b/crates/storage-pg/src/upstream_oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the repositories //! related to the upstream OAuth 2.0 providers @@ -20,7 +20,8 @@ pub use self::{ mod tests { use chrono::Duration; use mas_data_model::{ - UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod, + UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderOnBackchannelLogout, + UpstreamOAuthProviderTokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use mas_storage::{ @@ -29,7 +30,7 @@ mod tests { upstream_oauth2::{ UpstreamOAuthLinkFilter, UpstreamOAuthLinkRepository, UpstreamOAuthProviderFilter, UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository, - UpstreamOAuthSessionRepository, + UpstreamOAuthSessionFilter, UpstreamOAuthSessionRepository, }, user::UserRepository, }; @@ -78,6 +79,7 @@ mod tests { additional_authorization_parameters: Vec::new(), forward_login_hint: false, ui_order: 0, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, ) .await @@ -152,7 +154,7 @@ mod tests { let session = repo .upstream_oauth_session() - .complete_with_link(&clock, session, &link, None, None, None) + .complete_with_link(&clock, session, &link, None, None, None, None) .await .unwrap(); // Reload the session @@ -262,6 +264,29 @@ mod tests { 1 ); + // Test listing and counting sessions + let session_filter = UpstreamOAuthSessionFilter::new().for_provider(&provider); + + // Count the sessions for the provider + let session_count = repo + .upstream_oauth_session() + .count(session_filter) + .await + .unwrap(); + assert_eq!(session_count, 1); + + // List the sessions for the provider + let session_page = repo + .upstream_oauth_session() + .list(session_filter, Pagination::first(10)) + .await + .unwrap(); + + assert_eq!(session_page.edges.len(), 1); + assert_eq!(session_page.edges[0].id, session.id); + assert!(!session_page.has_next_page); + assert!(!session_page.has_previous_page); + // Try deleting the provider repo.upstream_oauth_provider() .delete(provider) @@ -326,6 +351,7 @@ mod tests { additional_authorization_parameters: Vec::new(), forward_login_hint: false, ui_order: 0, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, ) .await @@ -423,4 +449,203 @@ mod tests { .is_empty() ); } + + /// Test that the pagination works as expected in the upstream OAuth + /// session repository + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_session_repository_pagination(pool: PgPool) { + let scope = Scope::from_iter([OPENID]); + + let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42); + let clock = MockClock::default(); + let mut repo = PgRepository::from_pool(&pool).await.unwrap(); + + // Create a provider + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &clock, + UpstreamOAuthProviderParams { + issuer: Some("https://example.com/".to_owned()), + human_name: None, + brand_name: None, + scope, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + token_endpoint_signing_alg: None, + client_id: "client-id".to_owned(), + encrypted_client_secret: None, + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + token_endpoint_override: None, + authorization_endpoint_override: None, + userinfo_endpoint_override: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + ui_order: 0, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + }, + ) + .await + .unwrap(); + + let filter = UpstreamOAuthSessionFilter::new().for_provider(&provider); + + // Count the number of sessions before we start + assert_eq!( + repo.upstream_oauth_session().count(filter).await.unwrap(), + 0 + ); + + let mut links = Vec::with_capacity(3); + for subject in ["alice", "bob", "charlie"] { + let link = repo + .upstream_oauth_link() + .add(&mut rng, &clock, &provider, subject.to_owned(), None) + .await + .unwrap(); + links.push(link); + } + + let mut ids = Vec::with_capacity(20); + let sids = ["one", "two"].into_iter().cycle(); + // Create 20 sessions + for (idx, (link, sid)) in links.iter().cycle().zip(sids).enumerate().take(20) { + let state = format!("state-{idx}"); + let session = repo + .upstream_oauth_session() + .add(&mut rng, &clock, &provider, state, None, None) + .await + .unwrap(); + let id_token_claims = serde_json::json!({ + "sub": link.subject, + "sid": sid, + "aud": provider.client_id, + "iss": "https://example.com/", + }); + let session = repo + .upstream_oauth_session() + .complete_with_link( + &clock, + session, + link, + None, + Some(id_token_claims), + None, + None, + ) + .await + .unwrap(); + ids.push(session.id); + clock.advance(Duration::microseconds(10 * 1000 * 1000)); + } + + // Now we have 20 sessions + assert_eq!( + repo.upstream_oauth_session().count(filter).await.unwrap(), + 20 + ); + + // Lookup the first 10 items + let page = repo + .upstream_oauth_session() + .list(filter, Pagination::first(10)) + .await + .unwrap(); + + // It returned the first 10 items + assert!(page.has_next_page); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + assert_eq!(&edge_ids, &ids[..10]); + + // Lookup the next 10 items + let page = repo + .upstream_oauth_session() + .list(filter, Pagination::first(10).after(ids[9])) + .await + .unwrap(); + + // It returned the next 10 items + assert!(!page.has_next_page); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + assert_eq!(&edge_ids, &ids[10..]); + + // Lookup the last 10 items + let page = repo + .upstream_oauth_session() + .list(filter, Pagination::last(10)) + .await + .unwrap(); + + // It returned the last 10 items + assert!(page.has_previous_page); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + assert_eq!(&edge_ids, &ids[10..]); + + // Lookup the previous 10 items + let page = repo + .upstream_oauth_session() + .list(filter, Pagination::last(10).before(ids[10])) + .await + .unwrap(); + + // It returned the previous 10 items + assert!(!page.has_previous_page); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + assert_eq!(&edge_ids, &ids[..10]); + + // Lookup 5 items between two IDs + let page = repo + .upstream_oauth_session() + .list(filter, Pagination::first(10).after(ids[5]).before(ids[11])) + .await + .unwrap(); + + // It returned the items in between + assert!(!page.has_next_page); + let edge_ids: Vec<_> = page.edges.iter().map(|s| s.id).collect(); + assert_eq!(&edge_ids, &ids[6..11]); + + // Check the sub/sid filters + assert_eq!( + repo.upstream_oauth_session() + .count(filter.with_sub_claim("alice").with_sid_claim("one")) + .await + .unwrap(), + 4 + ); + assert_eq!( + repo.upstream_oauth_session() + .count(filter.with_sub_claim("bob").with_sid_claim("two")) + .await + .unwrap(), + 4 + ); + + let page = repo + .upstream_oauth_session() + .list( + filter.with_sub_claim("alice").with_sid_claim("one"), + Pagination::first(10), + ) + .await + .unwrap(); + assert_eq!(page.edges.len(), 4); + for edge in page.edges { + assert_eq!( + edge.id_token_claims().unwrap().get("sub").unwrap().as_str(), + Some("alice") + ); + assert_eq!( + edge.id_token_claims().unwrap().get("sid").unwrap().as_str(), + Some("one") + ); + } + } } diff --git a/crates/storage-pg/src/upstream_oauth2/provider.rs b/crates/storage-pg/src/upstream_oauth2/provider.rs index 879d7c658..364a81c90 100644 --- a/crates/storage-pg/src/upstream_oauth2/provider.rs +++ b/crates/storage-pg/src/upstream_oauth2/provider.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -71,12 +71,12 @@ struct ProviderLookup { response_mode: Option, additional_parameters: Option>>, forward_login_hint: bool, + on_backchannel_logout: String, } impl TryFrom for UpstreamOAuthProvider { type Error = DatabaseInconsistencyError; - #[allow(clippy::too_many_lines)] fn try_from(value: ProviderLookup) -> Result { let id = value.upstream_oauth_provider_id.into(); let scope = value.scope.parse().map_err(|e| { @@ -194,6 +194,13 @@ impl TryFrom for UpstreamOAuthProvider { .map(|Json(x)| x) .unwrap_or_default(); + let on_backchannel_logout = value.on_backchannel_logout.parse().map_err(|e| { + DatabaseInconsistencyError::on("upstream_oauth_providers") + .column("on_backchannel_logout") + .row(id) + .source(e) + })?; + Ok(UpstreamOAuthProvider { id, issuer: value.issuer, @@ -219,6 +226,7 @@ impl TryFrom for UpstreamOAuthProvider { response_mode, additional_authorization_parameters, forward_login_hint: value.forward_login_hint, + on_backchannel_logout, }) } } @@ -277,7 +285,8 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { pkce_mode, response_mode, additional_parameters as "additional_parameters: Json>", - forward_login_hint + forward_login_hint, + on_backchannel_logout FROM upstream_oauth_providers WHERE upstream_oauth_provider_id = $1 "#, @@ -340,9 +349,11 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { pkce_mode, response_mode, forward_login_hint, + on_backchannel_logout, created_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, - $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, + $12, $13, $14, $15, $16, $17, $18, $19, $20, + $21, $22, $23) "#, Uuid::from(id), params.issuer.as_deref(), @@ -380,6 +391,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { params.pkce_mode.as_str(), params.response_mode.as_ref().map(ToString::to_string), params.forward_login_hint, + params.on_backchannel_logout.as_str(), created_at, ) .traced() @@ -410,6 +422,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { pkce_mode: params.pkce_mode, response_mode: params.response_mode, additional_authorization_parameters: params.additional_authorization_parameters, + on_backchannel_logout: params.on_backchannel_logout, forward_login_hint: params.forward_login_hint, }) } @@ -525,10 +538,11 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { additional_parameters, forward_login_hint, ui_order, + on_backchannel_logout, created_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, - $12, $13, $14, $15, $16, $17, $18, $19, $20, - $21, $22, $23, $24) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, + $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, + $21, $22, $23, $24, $25) ON CONFLICT (upstream_oauth_provider_id) DO UPDATE SET @@ -554,7 +568,8 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { response_mode = EXCLUDED.response_mode, additional_parameters = EXCLUDED.additional_parameters, forward_login_hint = EXCLUDED.forward_login_hint, - ui_order = EXCLUDED.ui_order + ui_order = EXCLUDED.ui_order, + on_backchannel_logout = EXCLUDED.on_backchannel_logout RETURNING created_at "#, Uuid::from(id), @@ -595,6 +610,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { Json(¶ms.additional_authorization_parameters) as _, params.forward_login_hint, params.ui_order, + params.on_backchannel_logout.as_str(), created_at, ) .traced() @@ -626,6 +642,7 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { response_mode: params.response_mode, additional_authorization_parameters: params.additional_authorization_parameters, forward_login_hint: params.forward_login_hint, + on_backchannel_logout: params.on_backchannel_logout, }) } @@ -843,6 +860,13 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { )), ProviderLookupIden::ForwardLoginHint, ) + .expr_as( + Expr::col(( + UpstreamOAuthProviders::Table, + UpstreamOAuthProviders::OnBackchannelLogout, + )), + ProviderLookupIden::OnBackchannelLogout, + ) .from(UpstreamOAuthProviders::Table) .apply_filter(filter) .generate_pagination( @@ -936,7 +960,8 @@ impl UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<'_> { pkce_mode, response_mode, additional_parameters as "additional_parameters: Json>", - forward_login_hint + forward_login_hint, + on_backchannel_logout FROM upstream_oauth_providers WHERE disabled_at IS NULL ORDER BY ui_order ASC, upstream_oauth_provider_id ASC diff --git a/crates/storage-pg/src/upstream_oauth2/session.rs b/crates/storage-pg/src/upstream_oauth2/session.rs index 594f3be4c..8cc04eeb6 100644 --- a/crates/storage-pg/src/upstream_oauth2/session.rs +++ b/crates/storage-pg/src/upstream_oauth2/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -10,13 +10,53 @@ use mas_data_model::{ UpstreamOAuthAuthorizationSession, UpstreamOAuthAuthorizationSessionState, UpstreamOAuthLink, UpstreamOAuthProvider, }; -use mas_storage::{Clock, upstream_oauth2::UpstreamOAuthSessionRepository}; +use mas_storage::{ + Clock, Page, Pagination, + upstream_oauth2::{UpstreamOAuthSessionFilter, UpstreamOAuthSessionRepository}, +}; use rand::RngCore; +use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def, extension::postgres::PgExpr}; +use sea_query_binder::SqlxBinder; use sqlx::PgConnection; use ulid::Ulid; use uuid::Uuid; -use crate::{DatabaseError, DatabaseInconsistencyError, tracing::ExecuteExt}; +use crate::{ + DatabaseError, DatabaseInconsistencyError, + filter::{Filter, StatementExt}, + iden::UpstreamOAuthAuthorizationSessions, + pagination::QueryBuilderExt, + tracing::ExecuteExt, +}; + +impl Filter for UpstreamOAuthSessionFilter<'_> { + fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition { + sea_query::Condition::all() + .add_option(self.provider().map(|provider| { + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthProviderId, + )) + .eq(Uuid::from(provider.id)) + })) + .add_option(self.sub_claim().map(|sub| { + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::IdTokenClaims, + )) + .cast_json_field("sub") + .eq(sub) + })) + .add_option(self.sid_claim().map(|sid| { + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::IdTokenClaims, + )) + .cast_json_field("sid") + .eq(sid) + })) + } +} /// An implementation of [`UpstreamOAuthSessionRepository`] for a PostgreSQL /// connection @@ -32,6 +72,8 @@ impl<'c> PgUpstreamOAuthSessionRepository<'c> { } } +#[derive(sqlx::FromRow)] +#[enum_def] struct SessionLookup { upstream_oauth_authorization_session_id: Uuid, upstream_oauth_provider_id: Uuid, @@ -40,6 +82,7 @@ struct SessionLookup { code_challenge_verifier: Option, nonce: Option, id_token: Option, + id_token_claims: Option, userinfo: Option, created_at: DateTime, completed_at: Option>, @@ -56,18 +99,20 @@ impl TryFrom for UpstreamOAuthAuthorizationSession { let state = match ( value.upstream_oauth_link_id, value.id_token, + value.id_token_claims, value.extra_callback_parameters, value.userinfo, value.completed_at, value.consumed_at, value.unlinked_at, ) { - (None, None, None, None, None, None, None) => { + (None, None, None, None, None, None, None, None) => { UpstreamOAuthAuthorizationSessionState::Pending } ( Some(link_id), id_token, + id_token_claims, extra_callback_parameters, userinfo, Some(completed_at), @@ -77,12 +122,14 @@ impl TryFrom for UpstreamOAuthAuthorizationSession { completed_at, link_id: link_id.into(), id_token, + id_token_claims, extra_callback_parameters, userinfo, }, ( Some(link_id), id_token, + id_token_claims, extra_callback_parameters, userinfo, Some(completed_at), @@ -92,18 +139,27 @@ impl TryFrom for UpstreamOAuthAuthorizationSession { completed_at, link_id: link_id.into(), id_token, + id_token_claims, extra_callback_parameters, userinfo, consumed_at, }, - (_, id_token, _, _, Some(completed_at), consumed_at, Some(unlinked_at)) => { - UpstreamOAuthAuthorizationSessionState::Unlinked { - completed_at, - id_token, - consumed_at, - unlinked_at, - } - } + ( + _, + id_token, + id_token_claims, + _, + _, + Some(completed_at), + consumed_at, + Some(unlinked_at), + ) => UpstreamOAuthAuthorizationSessionState::Unlinked { + completed_at, + id_token, + id_token_claims, + consumed_at, + unlinked_at, + }, _ => { return Err(DatabaseInconsistencyError::on( "upstream_oauth_authorization_sessions", @@ -152,6 +208,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> { code_challenge_verifier, nonce, id_token, + id_token_claims, extra_callback_parameters, userinfo, created_at, @@ -253,6 +310,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> { upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession, upstream_oauth_link: &UpstreamOAuthLink, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, ) -> Result { @@ -261,16 +319,18 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> { sqlx::query!( r#" UPDATE upstream_oauth_authorization_sessions - SET upstream_oauth_link_id = $1, - completed_at = $2, - id_token = $3, - extra_callback_parameters = $4, - userinfo = $5 - WHERE upstream_oauth_authorization_session_id = $6 + SET upstream_oauth_link_id = $1 + , completed_at = $2 + , id_token = $3 + , id_token_claims = $4 + , extra_callback_parameters = $5 + , userinfo = $6 + WHERE upstream_oauth_authorization_session_id = $7 "#, Uuid::from(upstream_oauth_link.id), completed_at, id_token, + id_token_claims, extra_callback_parameters, userinfo, Uuid::from(upstream_oauth_authorization_session.id), @@ -284,6 +344,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> { completed_at, upstream_oauth_link, id_token, + id_token_claims, extra_callback_parameters, userinfo, ) @@ -327,4 +388,173 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> { Ok(upstream_oauth_authorization_session) } + + #[tracing::instrument( + name = "db.upstream_oauth_authorization_session.list", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn list( + &mut self, + filter: UpstreamOAuthSessionFilter<'_>, + pagination: Pagination, + ) -> Result, Self::Error> { + let (sql, arguments) = Query::select() + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthAuthorizationSessionId, + )), + SessionLookupIden::UpstreamOauthAuthorizationSessionId, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthProviderId, + )), + SessionLookupIden::UpstreamOauthProviderId, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthLinkId, + )), + SessionLookupIden::UpstreamOauthLinkId, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::State, + )), + SessionLookupIden::State, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::CodeChallengeVerifier, + )), + SessionLookupIden::CodeChallengeVerifier, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::Nonce, + )), + SessionLookupIden::Nonce, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::IdToken, + )), + SessionLookupIden::IdToken, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::IdTokenClaims, + )), + SessionLookupIden::IdTokenClaims, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::ExtraCallbackParameters, + )), + SessionLookupIden::ExtraCallbackParameters, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::Userinfo, + )), + SessionLookupIden::Userinfo, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::CreatedAt, + )), + SessionLookupIden::CreatedAt, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::CompletedAt, + )), + SessionLookupIden::CompletedAt, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::ConsumedAt, + )), + SessionLookupIden::ConsumedAt, + ) + .expr_as( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UnlinkedAt, + )), + SessionLookupIden::UnlinkedAt, + ) + .from(UpstreamOAuthAuthorizationSessions::Table) + .apply_filter(filter) + .generate_pagination( + ( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthAuthorizationSessionId, + ), + pagination, + ) + .build_sqlx(PostgresQueryBuilder); + + let edges: Vec = sqlx::query_as_with(&sql, arguments) + .traced() + .fetch_all(&mut *self.conn) + .await?; + + let page = pagination + .process(edges) + .try_map(UpstreamOAuthAuthorizationSession::try_from)?; + + Ok(page) + } + + #[tracing::instrument( + name = "db.upstream_oauth_authorization_session.count", + skip_all, + fields( + db.query.text, + ), + err, + )] + async fn count( + &mut self, + filter: UpstreamOAuthSessionFilter<'_>, + ) -> Result { + let (sql, arguments) = Query::select() + .expr( + Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthAuthorizationSessionId, + )) + .count(), + ) + .from(UpstreamOAuthAuthorizationSessions::Table) + .apply_filter(filter) + .build_sqlx(PostgresQueryBuilder); + + let count: i64 = sqlx::query_scalar_with(&sql, arguments) + .traced() + .fetch_one(&mut *self.conn) + .await?; + + count + .try_into() + .map_err(DatabaseError::to_invalid_operation) + } } diff --git a/crates/storage-pg/src/user/email.rs b/crates/storage-pg/src/user/email.rs index ad8afd6a8..ba7f1a35f 100644 --- a/crates/storage-pg/src/user/email.rs +++ b/crates/storage-pg/src/user/email.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -15,7 +15,7 @@ use mas_storage::{ user::{UserEmailFilter, UserEmailRepository}, }; use rand::RngCore; -use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def}; +use sea_query::{Expr, Func, PostgresQueryBuilder, Query, SimpleExpr, enum_def}; use sea_query_binder::SqlxBinder; use sqlx::PgConnection; use ulid::Ulid; @@ -110,10 +110,13 @@ impl Filter for UserEmailFilter<'_> { .add_option(self.user().map(|user| { Expr::col((UserEmails::Table, UserEmails::UserId)).eq(Uuid::from(user.id)) })) - .add_option( - self.email() - .map(|email| Expr::col((UserEmails::Table, UserEmails::Email)).eq(email)), - ) + .add_option(self.email().map(|email| { + SimpleExpr::from(Func::lower(Expr::col(( + UserEmails::Table, + UserEmails::Email, + )))) + .eq(Func::lower(email)) + })) } } @@ -175,7 +178,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> { , created_at FROM user_emails - WHERE user_id = $1 AND email = $2 + WHERE user_id = $1 AND LOWER(email) = LOWER($2) "#, Uuid::from(user.id), email, @@ -209,7 +212,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> { , email , created_at FROM user_emails - WHERE email = $1 + WHERE LOWER(email) = LOWER($1) "#, email, ) diff --git a/crates/storage-pg/src/user/mod.rs b/crates/storage-pg/src/user/mod.rs index 8e755188d..6d03e9bf7 100644 --- a/crates/storage-pg/src/user/mod.rs +++ b/crates/storage-pg/src/user/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing the PostgreSQL implementation of the user-related //! repositories @@ -379,7 +379,40 @@ impl UserRepository for PgUserRepository<'_> { DatabaseError::ensure_affected_rows(&res, 1)?; - user.deactivated_at = Some(user.created_at); + user.deactivated_at = Some(deactivated_at); + + Ok(user) + } + + #[tracing::instrument( + name = "db.user.reactivate", + skip_all, + fields( + db.query.text, + %user.id, + ), + err, + )] + async fn reactivate(&mut self, mut user: User) -> Result { + if user.deactivated_at.is_none() { + return Ok(user); + } + + let res = sqlx::query!( + r#" + UPDATE users + SET deactivated_at = NULL + WHERE user_id = $1 + "#, + Uuid::from(user.id), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + user.deactivated_at = None; Ok(user) } diff --git a/crates/storage-pg/src/user/password.rs b/crates/storage-pg/src/user/password.rs index 6a1b19f0a..ad59a99b4 100644 --- a/crates/storage-pg/src/user/password.rs +++ b/crates/storage-pg/src/user/password.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/crates/storage-pg/src/user/recovery.rs b/crates/storage-pg/src/user/recovery.rs index bc108b52a..25de80a38 100644 --- a/crates/storage-pg/src/user/recovery.rs +++ b/crates/storage-pg/src/user/recovery.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/storage-pg/src/user/registration.rs b/crates/storage-pg/src/user/registration.rs index 7f123b361..e8c228771 100644 --- a/crates/storage-pg/src/user/registration.rs +++ b/crates/storage-pg/src/user/registration.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/storage-pg/src/user/registration_token.rs b/crates/storage-pg/src/user/registration_token.rs index f7a9ab54e..b748c4645 100644 --- a/crates/storage-pg/src/user/registration_token.rs +++ b/crates/storage-pg/src/user/registration_token.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -54,7 +54,6 @@ struct UserRegistrationTokenLookup { } impl Filter for UserRegistrationTokenFilter { - #[expect(clippy::too_many_lines)] fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition { sea_query::Condition::all() .add_option(self.has_been_used().map(|has_been_used| { diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index 3bea6781c..db8a8cacf 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; @@ -17,7 +17,7 @@ use mas_storage::{ user::{BrowserSessionFilter, BrowserSessionRepository}, }; use rand::RngCore; -use sea_query::{Expr, PostgresQueryBuilder}; +use sea_query::{Expr, PostgresQueryBuilder, Query}; use sea_query_binder::SqlxBinder; use sqlx::PgConnection; use ulid::Ulid; @@ -26,7 +26,7 @@ use uuid::Uuid; use crate::{ DatabaseError, DatabaseInconsistencyError, filter::StatementExt, - iden::{UserSessions, Users}, + iden::{UpstreamOAuthAuthorizationSessions, UserSessionAuthentications, UserSessions, Users}, pagination::QueryBuilderExt, tracing::ExecuteExt, }; @@ -145,6 +145,30 @@ impl crate::filter::Filter for BrowserSessionFilter<'_> { .add_option(self.last_active_before().map(|last_active_before| { Expr::col((UserSessions::Table, UserSessions::LastActiveAt)).lt(last_active_before) })) + .add_option(self.authenticated_by_upstream_sessions().map(|filter| { + // For filtering by upstream sessions, we need to hop over the + // `user_session_authentications` table + let join_expr = Expr::col(( + UserSessionAuthentications::Table, + UserSessionAuthentications::UpstreamOAuthAuthorizationSessionId, + )) + .eq(Expr::col(( + UpstreamOAuthAuthorizationSessions::Table, + UpstreamOAuthAuthorizationSessions::UpstreamOAuthAuthorizationSessionId, + ))); + + Expr::col((UserSessions::Table, UserSessions::UserSessionId)).in_subquery( + Query::select() + .expr(Expr::col(( + UserSessionAuthentications::Table, + UserSessionAuthentications::UserSessionId, + ))) + .from(UserSessionAuthentications::Table) + .inner_join(UpstreamOAuthAuthorizationSessions::Table, join_expr) + .apply_filter(filter) + .take(), + ) + })) } } diff --git a/crates/storage-pg/src/user/terms.rs b/crates/storage-pg/src/user/terms.rs index 9efa1f008..b40716569 100644 --- a/crates/storage-pg/src/user/terms.rs +++ b/crates/storage-pg/src/user/terms.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::User; diff --git a/crates/storage-pg/src/user/tests.rs b/crates/storage-pg/src/user/tests.rs index c37c7ff8e..3225c8bbd 100644 --- a/crates/storage-pg/src/user/tests.rs +++ b/crates/storage-pg/src/user/tests.rs @@ -1,18 +1,21 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::Duration; +use mas_iana::jose::JsonWebSignatureAlg; use mas_storage::{ Clock, Pagination, RepositoryAccess, clock::MockClock, + upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthSessionFilter}, user::{ BrowserSessionFilter, BrowserSessionRepository, UserEmailFilter, UserEmailRepository, UserFilter, UserPasswordRepository, UserRepository, }, }; +use oauth2_types::scope::{OPENID, Scope}; use rand::SeedableRng; use rand_chacha::ChaChaRng; use sqlx::PgPool; @@ -265,6 +268,10 @@ async fn test_user_repo_find_by_username(pool: PgPool) { async fn test_user_email_repo(pool: PgPool) { const USERNAME: &str = "john"; const EMAIL: &str = "john@example.com"; + // This is what is stored in the database, making sure that: + // 1. we don't normalize the email address when storing it + // 2. looking it up is case-incensitive + const UPPERCASE_EMAIL: &str = "JOHN@EXAMPLE.COM"; let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed(); let mut rng = ChaChaRng::seed_from_u64(42); @@ -292,12 +299,12 @@ async fn test_user_email_repo(pool: PgPool) { let user_email = repo .user_email() - .add(&mut rng, &clock, &user, EMAIL.to_owned()) + .add(&mut rng, &clock, &user, UPPERCASE_EMAIL.to_owned()) .await .unwrap(); assert_eq!(user_email.user_id, user.id); - assert_eq!(user_email.email, EMAIL); + assert_eq!(user_email.email, UPPERCASE_EMAIL); // Check the counts assert_eq!(repo.user_email().count(all).await.unwrap(), 1); @@ -318,7 +325,7 @@ async fn test_user_email_repo(pool: PgPool) { .expect("user email was not found"); assert_eq!(user_email.user_id, user.id); - assert_eq!(user_email.email, EMAIL); + assert_eq!(user_email.email, UPPERCASE_EMAIL); // Listing the user emails should work let emails = repo @@ -717,6 +724,100 @@ async fn test_user_session(pool: PgPool) { assert_eq!(repo.browser_session().count(all_bob).await.unwrap(), 5); assert_eq!(repo.browser_session().count(active_bob).await.unwrap(), 0); assert_eq!(repo.browser_session().count(finished).await.unwrap(), 11); + + // Checking the 'authenticaated by upstream sessions' filter + // We need a provider + let provider = repo + .upstream_oauth_provider() + .add( + &mut rng, + &clock, + UpstreamOAuthProviderParams { + issuer: None, + human_name: None, + brand_name: None, + scope: Scope::from_iter([OPENID]), + token_endpoint_auth_method: + mas_data_model::UpstreamOAuthProviderTokenAuthMethod::None, + token_endpoint_signing_alg: None, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + fetch_userinfo: false, + userinfo_signed_response_alg: None, + client_id: "client".to_owned(), + encrypted_client_secret: None, + claims_imports: mas_data_model::UpstreamOAuthProviderClaimsImports::default(), + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + jwks_uri_override: None, + discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Disabled, + pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Disabled, + response_mode: None, + additional_authorization_parameters: Vec::new(), + forward_login_hint: false, + ui_order: 0, + on_backchannel_logout: + mas_data_model::UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + }, + ) + .await + .unwrap(); + + // Start a authorization session + let upstream_oauth_session = repo + .upstream_oauth_session() + .add(&mut rng, &clock, &provider, "state".to_owned(), None, None) + .await + .unwrap(); + + // Start a browser session + let session = repo + .browser_session() + .add(&mut rng, &clock, &alice, None) + .await + .unwrap(); + + // Make the session from alice authenticated by this session + repo.browser_session() + .authenticate_with_upstream(&mut rng, &clock, &session, &upstream_oauth_session) + .await + .unwrap(); + + // This will match all authorization sessions, which matches exactly that one + // authorization session + let upstream_oauth_session_filter = UpstreamOAuthSessionFilter::new(); + let filter = BrowserSessionFilter::new() + .authenticated_by_upstream_sessions_only(upstream_oauth_session_filter); + + // Now try to look it up + let page = repo + .browser_session() + .list(filter, Pagination::first(10)) + .await + .unwrap(); + assert_eq!(page.edges.len(), 1); + assert_eq!(page.edges[0].id, session.id); + + // Try counting + assert_eq!(repo.browser_session().count(filter).await.unwrap(), 1); + + // Try finishing the session + let affected = repo + .browser_session() + .finish_bulk(&clock, filter) + .await + .unwrap(); + assert_eq!(affected, 1); + + // Lookup the session by its ID + let lookup = repo + .browser_session() + .lookup(session.id) + .await + .unwrap() + .expect("session to be found in the database"); + // It should be finished + assert!(lookup.finished_at.is_some()); } #[sqlx::test(migrator = "crate::MIGRATOR")] diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index f22ef1d6e..07f4330c6 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-storage" version.workspace = true diff --git a/crates/storage/src/app_session.rs b/crates/storage/src/app_session.rs index fd1850d3d..65e5669b4 100644 --- a/crates/storage/src/app_session.rs +++ b/crates/storage/src/app_session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repositories to interact with all kinds of sessions diff --git a/crates/storage/src/clock.rs b/crates/storage/src/clock.rs index 6af9926e1..bf31835f0 100644 --- a/crates/storage/src/clock.rs +++ b/crates/storage/src/clock.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A [`Clock`] is a way to get the current date and time. //! @@ -15,7 +15,7 @@ use std::sync::{Arc, atomic::AtomicI64}; use chrono::{DateTime, TimeZone, Utc}; /// Represents a clock which can give the current date and time -pub trait Clock: Sync { +pub trait Clock: Send + Sync { /// Get the current date and time fn now(&self) -> DateTime; } diff --git a/crates/storage/src/compat/access_token.rs b/crates/storage/src/compat/access_token.rs index 87d244f4d..d927009b7 100644 --- a/crates/storage/src/compat/access_token.rs +++ b/crates/storage/src/compat/access_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::Duration; @@ -13,7 +13,7 @@ use ulid::Ulid; use crate::{Clock, repository_impl}; /// A [`CompatAccessTokenRepository`] helps interacting with -/// [`CompatAccessToken`] saved in the storage backend +/// [`CompatAccessToken`] saved in the storage backend #[async_trait] pub trait CompatAccessTokenRepository: Send + Sync { /// The error type returned by the repository diff --git a/crates/storage/src/compat/mod.rs b/crates/storage/src/compat/mod.rs index 3bb11717b..8c518d423 100644 --- a/crates/storage/src/compat/mod.rs +++ b/crates/storage/src/compat/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repositories to interact with entities of the compatibility layer diff --git a/crates/storage/src/compat/refresh_token.rs b/crates/storage/src/compat/refresh_token.rs index ff568ec9f..ca37278cc 100644 --- a/crates/storage/src/compat/refresh_token.rs +++ b/crates/storage/src/compat/refresh_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{CompatAccessToken, CompatRefreshToken, CompatSession}; diff --git a/crates/storage/src/compat/session.rs b/crates/storage/src/compat/session.rs index e935e986b..5287b4cee 100644 --- a/crates/storage/src/compat/session.rs +++ b/crates/storage/src/compat/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; @@ -12,7 +12,7 @@ use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User use rand_core::RngCore; use ulid::Ulid; -use crate::{Clock, Page, Pagination, repository_impl}; +use crate::{Clock, Page, Pagination, repository_impl, user::BrowserSessionFilter}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CompatSessionState { @@ -59,6 +59,7 @@ impl CompatSessionType { pub struct CompatSessionFilter<'a> { user: Option<&'a User>, browser_session: Option<&'a BrowserSession>, + browser_session_filter: Option>, state: Option, auth_type: Option, device: Option<&'a Device>, @@ -106,12 +107,28 @@ impl<'a> CompatSessionFilter<'a> { self } + /// Set the browser sessions filter + #[must_use] + pub fn for_browser_sessions( + mut self, + browser_session_filter: BrowserSessionFilter<'a>, + ) -> Self { + self.browser_session_filter = Some(browser_session_filter); + self + } + /// Get the browser session filter #[must_use] pub fn browser_session(&self) -> Option<&'a BrowserSession> { self.browser_session } + /// Get the browser sessions filter + #[must_use] + pub fn browser_session_filter(&self) -> Option> { + self.browser_session_filter + } + /// Only return sessions with a last active time before the given time #[must_use] pub fn with_last_active_before(mut self, last_active_before: DateTime) -> Self { diff --git a/crates/storage/src/compat/sso_login.rs b/crates/storage/src/compat/sso_login.rs index 08e8c5491..90cc42750 100644 --- a/crates/storage/src/compat/sso_login.rs +++ b/crates/storage/src/compat/sso_login.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, User}; diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 07d8bd97c..11191e092 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Interactions with the storage backend //! diff --git a/crates/storage/src/oauth2/access_token.rs b/crates/storage/src/oauth2/access_token.rs index 9a128bbdb..1ffc4d82d 100644 --- a/crates/storage/src/oauth2/access_token.rs +++ b/crates/storage/src/oauth2/access_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::Duration; diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index cb4802a92..f61f6b8c0 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{AuthorizationCode, AuthorizationGrant, Client, Session}; diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs index 33b92d189..bf2d10a29 100644 --- a/crates/storage/src/oauth2/client.rs +++ b/crates/storage/src/oauth2/client.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::{BTreeMap, BTreeSet}; @@ -17,7 +17,7 @@ use url::Url; use crate::{Clock, repository_impl}; -/// An [`OAuth2ClientRepository`] helps interacting with [`Client`] saved in the +/// An [`OAuth2ClientRepository`] helps interacting with [`Client`] saved in the /// storage backend #[async_trait] pub trait OAuth2ClientRepository: Send + Sync { diff --git a/crates/storage/src/oauth2/device_code_grant.rs b/crates/storage/src/oauth2/device_code_grant.rs index 762e854cc..89bcb61f1 100644 --- a/crates/storage/src/oauth2/device_code_grant.rs +++ b/crates/storage/src/oauth2/device_code_grant.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/storage/src/oauth2/mod.rs b/crates/storage/src/oauth2/mod.rs index 8f6b86056..a2d172567 100644 --- a/crates/storage/src/oauth2/mod.rs +++ b/crates/storage/src/oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repositories to interact with entities related to the OAuth 2.0 protocol diff --git a/crates/storage/src/oauth2/refresh_token.rs b/crates/storage/src/oauth2/refresh_token.rs index 7f3928906..b50295e4f 100644 --- a/crates/storage/src/oauth2/refresh_token.rs +++ b/crates/storage/src/oauth2/refresh_token.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{AccessToken, RefreshToken, Session}; diff --git a/crates/storage/src/oauth2/session.rs b/crates/storage/src/oauth2/session.rs index 07f91a2b0..5d217c1e2 100644 --- a/crates/storage/src/oauth2/session.rs +++ b/crates/storage/src/oauth2/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; @@ -13,7 +13,7 @@ use oauth2_types::scope::Scope; use rand_core::RngCore; use ulid::Ulid; -use crate::{Clock, Pagination, pagination::Page, repository_impl}; +use crate::{Clock, Pagination, pagination::Page, repository_impl, user::BrowserSessionFilter}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OAuth2SessionState { @@ -49,6 +49,7 @@ pub struct OAuth2SessionFilter<'a> { user: Option<&'a User>, any_user: Option, browser_session: Option<&'a BrowserSession>, + browser_session_filter: Option>, device: Option<&'a Device>, client: Option<&'a Client>, client_kind: Option, @@ -109,6 +110,16 @@ impl<'a> OAuth2SessionFilter<'a> { self } + /// List sessions started by a set of browser sessions + #[must_use] + pub fn for_browser_sessions( + mut self, + browser_session_filter: BrowserSessionFilter<'a>, + ) -> Self { + self.browser_session_filter = Some(browser_session_filter); + self + } + /// Get the browser session filter /// /// Returns [`None`] if no browser session filter was set @@ -117,6 +128,14 @@ impl<'a> OAuth2SessionFilter<'a> { self.browser_session } + /// Get the browser sessions filter + /// + /// Returns [`None`] if no browser session filter was set + #[must_use] + pub fn browser_session_filter(&self) -> Option> { + self.browser_session_filter + } + /// List sessions for a specific client #[must_use] pub fn for_client(mut self, client: &'a Client) -> Self { diff --git a/crates/storage/src/pagination.rs b/crates/storage/src/pagination.rs index 318caff70..01b8ed197 100644 --- a/crates/storage/src/pagination.rs +++ b/crates/storage/src/pagination.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Utilities to manage paginated queries. diff --git a/crates/storage/src/policy_data.rs b/crates/storage/src/policy_data.rs index 6c7e5d89f..ec3b29c5a 100644 --- a/crates/storage/src/policy_data.rs +++ b/crates/storage/src/policy_data.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repositories to interact with the policy data saved in the storage backend. diff --git a/crates/storage/src/queue/job.rs b/crates/storage/src/queue/job.rs index bc07c3b83..30a5368c9 100644 --- a/crates/storage/src/queue/job.rs +++ b/crates/storage/src/queue/job.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repository to interact with jobs in the job queue diff --git a/crates/storage/src/queue/mod.rs b/crates/storage/src/queue/mod.rs index 03d969bbb..958ae13d0 100644 --- a/crates/storage/src/queue/mod.rs +++ b/crates/storage/src/queue/mod.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! A module containing repositories for the job queue diff --git a/crates/storage/src/queue/schedule.rs b/crates/storage/src/queue/schedule.rs index aaa83e5d2..32c225eca 100644 --- a/crates/storage/src/queue/schedule.rs +++ b/crates/storage/src/queue/schedule.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repository to interact with recurrent scheduled jobs in the job queue diff --git a/crates/storage/src/queue/tasks.rs b/crates/storage/src/queue/tasks.rs index b0075f319..eb16f6e29 100644 --- a/crates/storage/src/queue/tasks.rs +++ b/crates/storage/src/queue/tasks.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use chrono::{DateTime, Utc}; use mas_data_model::{ diff --git a/crates/storage/src/queue/worker.rs b/crates/storage/src/queue/worker.rs index 23fa96104..d2d96ecfa 100644 --- a/crates/storage/src/queue/worker.rs +++ b/crates/storage/src/queue/worker.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repository to interact with workers in the job queue diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs index a02edb4ad..518769eb1 100644 --- a/crates/storage/src/repository.rs +++ b/crates/storage/src/repository.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use futures_util::future::BoxFuture; diff --git a/crates/storage/src/upstream_oauth2/link.rs b/crates/storage/src/upstream_oauth2/link.rs index cca070d86..a3672f361 100644 --- a/crates/storage/src/upstream_oauth2/link.rs +++ b/crates/storage/src/upstream_oauth2/link.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{UpstreamOAuthLink, UpstreamOAuthProvider, User}; diff --git a/crates/storage/src/upstream_oauth2/mod.rs b/crates/storage/src/upstream_oauth2/mod.rs index c2f72f278..39fefffe8 100644 --- a/crates/storage/src/upstream_oauth2/mod.rs +++ b/crates/storage/src/upstream_oauth2/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repositories to interact with entities related to the upstream OAuth 2.0 //! providers @@ -16,5 +16,5 @@ pub use self::{ provider::{ UpstreamOAuthProviderFilter, UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository, }, - session::UpstreamOAuthSessionRepository, + session::{UpstreamOAuthSessionFilter, UpstreamOAuthSessionRepository}, }; diff --git a/crates/storage/src/upstream_oauth2/provider.rs b/crates/storage/src/upstream_oauth2/provider.rs index ac6553b88..bc44bfab7 100644 --- a/crates/storage/src/upstream_oauth2/provider.rs +++ b/crates/storage/src/upstream_oauth2/provider.rs @@ -1,16 +1,16 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::marker::PhantomData; use async_trait::async_trait; use mas_data_model::{ UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, - UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderResponseMode, - UpstreamOAuthProviderTokenAuthMethod, + UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode, + UpstreamOAuthProviderResponseMode, UpstreamOAuthProviderTokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use oauth2_types::scope::Scope; @@ -101,6 +101,9 @@ pub struct UpstreamOAuthProviderParams { /// The position of the provider in the UI pub ui_order: i32, + + /// The behavior when receiving a backchannel logout notification + pub on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout, } /// Filter parameters for listing upstream OAuth 2.0 providers diff --git a/crates/storage/src/upstream_oauth2/session.rs b/crates/storage/src/upstream_oauth2/session.rs index c563fce5e..d6505285b 100644 --- a/crates/storage/src/upstream_oauth2/session.rs +++ b/crates/storage/src/upstream_oauth2/session.rs @@ -1,15 +1,76 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{UpstreamOAuthAuthorizationSession, UpstreamOAuthLink, UpstreamOAuthProvider}; use rand_core::RngCore; use ulid::Ulid; -use crate::{Clock, repository_impl}; +use crate::{Clock, Pagination, pagination::Page, repository_impl}; + +/// Filter parameters for listing upstream OAuth sessions +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct UpstreamOAuthSessionFilter<'a> { + provider: Option<&'a UpstreamOAuthProvider>, + sub_claim: Option<&'a str>, + sid_claim: Option<&'a str>, +} + +impl<'a> UpstreamOAuthSessionFilter<'a> { + /// Create a new [`UpstreamOAuthSessionFilter`] with default values + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Set the upstream OAuth provider for which to list sessions + #[must_use] + pub fn for_provider(mut self, provider: &'a UpstreamOAuthProvider) -> Self { + self.provider = Some(provider); + self + } + + /// Get the upstream OAuth provider filter + /// + /// Returns [`None`] if no filter was set + #[must_use] + pub fn provider(&self) -> Option<&UpstreamOAuthProvider> { + self.provider + } + + /// Set the `sub` claim to filter by + #[must_use] + pub fn with_sub_claim(mut self, sub_claim: &'a str) -> Self { + self.sub_claim = Some(sub_claim); + self + } + + /// Get the `sub` claim filter + /// + /// Returns [`None`] if no filter was set + #[must_use] + pub fn sub_claim(&self) -> Option<&str> { + self.sub_claim + } + + /// Set the `sid` claim to filter by + #[must_use] + pub fn with_sid_claim(mut self, sid_claim: &'a str) -> Self { + self.sid_claim = Some(sid_claim); + self + } + + /// Get the `sid` claim filter + /// + /// Returns [`None`] if no filter was set + #[must_use] + pub fn sid_claim(&self) -> Option<&str> { + self.sid_claim + } +} /// An [`UpstreamOAuthSessionRepository`] helps interacting with /// [`UpstreamOAuthAuthorizationSession`] saved in the storage backend @@ -74,18 +135,23 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync { /// * `upstream_oauth_link`: the link to associate with the session /// * `id_token`: the ID token returned by the upstream OAuth provider, if /// present + /// * `id_token_claims`: the claims contained in the ID token, if present /// * `extra_callback_parameters`: the extra query parameters returned in /// the callback, if any + /// * `userinfo`: the user info returned by the upstream OAuth provider, if + /// requested /// /// # Errors /// /// Returns [`Self::Error`] if the underlying repository fails + #[expect(clippy::too_many_arguments)] async fn complete_with_link( &mut self, clock: &dyn Clock, upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession, upstream_oauth_link: &UpstreamOAuthLink, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, ) -> Result; @@ -107,6 +173,36 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync { clock: &dyn Clock, upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession, ) -> Result; + + /// List [`UpstreamOAuthAuthorizationSession`] with the given filter and + /// pagination + /// + /// # Parameters + /// + /// * `filter`: The filter to apply + /// * `pagination`: The pagination parameters + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn list( + &mut self, + filter: UpstreamOAuthSessionFilter<'_>, + pagination: Pagination, + ) -> Result, Self::Error>; + + /// Count the number of [`UpstreamOAuthAuthorizationSession`] with the given + /// filter + /// + /// # Parameters + /// + /// * `filter`: The filter to apply + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn count(&mut self, filter: UpstreamOAuthSessionFilter<'_>) + -> Result; } repository_impl!(UpstreamOAuthSessionRepository: @@ -131,6 +227,7 @@ repository_impl!(UpstreamOAuthSessionRepository: upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession, upstream_oauth_link: &UpstreamOAuthLink, id_token: Option, + id_token_claims: Option, extra_callback_parameters: Option, userinfo: Option, ) -> Result; @@ -140,4 +237,12 @@ repository_impl!(UpstreamOAuthSessionRepository: clock: &dyn Clock, upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession, ) -> Result; + + async fn list( + &mut self, + filter: UpstreamOAuthSessionFilter<'_>, + pagination: Pagination, + ) -> Result, Self::Error>; + + async fn count(&mut self, filter: UpstreamOAuthSessionFilter<'_>) -> Result; ); diff --git a/crates/storage/src/user/email.rs b/crates/storage/src/user/email.rs index 4cdc8d665..daaead638 100644 --- a/crates/storage/src/user/email.rs +++ b/crates/storage/src/user/email.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{ @@ -36,6 +36,8 @@ impl<'a> UserEmailFilter<'a> { } /// Filter for emails matching a specific email address + /// + /// The email address is case-insensitive #[must_use] pub fn for_email(mut self, email: &'a str) -> Self { self.email = Some(email); @@ -81,6 +83,8 @@ pub trait UserEmailRepository: Send + Sync { /// Lookup an [`UserEmail`] by its email address for a [`User`] /// + /// The email address is case-insensitive + /// /// Returns `None` if no matching [`UserEmail`] was found /// /// # Parameters @@ -95,6 +99,8 @@ pub trait UserEmailRepository: Send + Sync { /// Lookup an [`UserEmail`] by its email address /// + /// The email address is case-insensitive + /// /// Returns `None` if no matching [`UserEmail`] was found or if multiple /// [`UserEmail`] are found /// diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index 17852f0e9..f864157b1 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Repositories to interact with entities related to user accounts @@ -244,6 +244,19 @@ pub trait UserRepository: Send + Sync { /// Returns [`Self::Error`] if the underlying repository fails async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result; + /// Reactivate a [`User`] + /// + /// Returns the reactivated [`User`] + /// + /// # Parameters + /// + /// * `user`: The [`User`] to reactivate + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn reactivate(&mut self, user: User) -> Result; + /// Set whether a [`User`] can request admin /// /// Returns the [`User`] with the new `can_request_admin` value @@ -315,6 +328,7 @@ repository_impl!(UserRepository: async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result; async fn unlock(&mut self, user: User) -> Result; async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result; + async fn reactivate(&mut self, user: User) -> Result; async fn set_can_request_admin( &mut self, user: User, diff --git a/crates/storage/src/user/password.rs b/crates/storage/src/user/password.rs index fe7115d97..a4e01957f 100644 --- a/crates/storage/src/user/password.rs +++ b/crates/storage/src/user/password.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::{Password, User}; diff --git a/crates/storage/src/user/recovery.rs b/crates/storage/src/user/recovery.rs index a5361e795..81f17acc7 100644 --- a/crates/storage/src/user/recovery.rs +++ b/crates/storage/src/user/recovery.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/storage/src/user/registration.rs b/crates/storage/src/user/registration.rs index 49fd01fdc..de7b6ef1b 100644 --- a/crates/storage/src/user/registration.rs +++ b/crates/storage/src/user/registration.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; diff --git a/crates/storage/src/user/registration_token.rs b/crates/storage/src/user/registration_token.rs index e3913b5d8..ef37c7a84 100644 --- a/crates/storage/src/user/registration_token.rs +++ b/crates/storage/src/user/registration_token.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::{DateTime, Utc}; diff --git a/crates/storage/src/user/session.rs b/crates/storage/src/user/session.rs index 2421ff009..23736bbfc 100644 --- a/crates/storage/src/user/session.rs +++ b/crates/storage/src/user/session.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::net::IpAddr; @@ -14,7 +14,10 @@ use mas_data_model::{ use rand_core::RngCore; use ulid::Ulid; -use crate::{Clock, Pagination, pagination::Page, repository_impl}; +use crate::{ + Clock, Pagination, pagination::Page, repository_impl, + upstream_oauth2::UpstreamOAuthSessionFilter, +}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BrowserSessionState { @@ -39,6 +42,7 @@ pub struct BrowserSessionFilter<'a> { state: Option, last_active_before: Option>, last_active_after: Option>, + authenticated_by_upstream_sessions: Option>, } impl<'a> BrowserSessionFilter<'a> { @@ -110,6 +114,23 @@ impl<'a> BrowserSessionFilter<'a> { pub fn state(&self) -> Option { self.state } + + /// Only return browser sessions authenticated by the given upstream OAuth + /// sessions + #[must_use] + pub fn authenticated_by_upstream_sessions_only( + mut self, + filter: UpstreamOAuthSessionFilter<'a>, + ) -> Self { + self.authenticated_by_upstream_sessions = Some(filter); + self + } + + /// Get the upstream OAuth session filter + #[must_use] + pub fn authenticated_by_upstream_sessions(&self) -> Option> { + self.authenticated_by_upstream_sessions + } } /// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`] diff --git a/crates/storage/src/user/terms.rs b/crates/storage/src/user/terms.rs index 34a6d3a6b..d24f9272b 100644 --- a/crates/storage/src/user/terms.rs +++ b/crates/storage/src/user/terms.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use mas_data_model::User; diff --git a/crates/storage/src/utils.rs b/crates/storage/src/utils.rs index 9f5d7ab98..ad1aacbb7 100644 --- a/crates/storage/src/utils.rs +++ b/crates/storage/src/utils.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Wrappers and useful type aliases diff --git a/crates/syn2mas/Cargo.toml b/crates/syn2mas/Cargo.toml index 5452c14be..9dfee658b 100644 --- a/crates/syn2mas/Cargo.toml +++ b/crates/syn2mas/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "syn2mas" version.workspace = true diff --git a/crates/syn2mas/src/lib.rs b/crates/syn2mas/src/lib.rs index 703e53150..3593858d7 100644 --- a/crates/syn2mas/src/lib.rs +++ b/crates/syn2mas/src/lib.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod mas_writer; mod synapse_reader; diff --git a/crates/syn2mas/src/mas_writer/checks.rs b/crates/syn2mas/src/mas_writer/checks.rs index 288156d8c..ae0964bff 100644 --- a/crates/syn2mas/src/mas_writer/checks.rs +++ b/crates/syn2mas/src/mas_writer/checks.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! # MAS Database Checks //! diff --git a/crates/syn2mas/src/mas_writer/constraint_pausing.rs b/crates/syn2mas/src/mas_writer/constraint_pausing.rs index 49fd4a8e3..3bfef602f 100644 --- a/crates/syn2mas/src/mas_writer/constraint_pausing.rs +++ b/crates/syn2mas/src/mas_writer/constraint_pausing.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::time::Instant; diff --git a/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql b/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql index 9da09b174..957cedcb3 100644 --- a/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql +++ b/crates/syn2mas/src/mas_writer/fixtures/upstream_provider.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO upstream_oauth_providers ( diff --git a/crates/syn2mas/src/mas_writer/locking.rs b/crates/syn2mas/src/mas_writer/locking.rs index 8200924d4..96fd2d30f 100644 --- a/crates/syn2mas/src/mas_writer/locking.rs +++ b/crates/syn2mas/src/mas_writer/locking.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index f36851dfd..6d59ec4b9 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -1,7 +1,7 @@ // Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! # MAS Writer //! diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap index a368aa9a5..adb6d4ee4 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap @@ -25,6 +25,7 @@ upstream_oauth_providers: id_token_signed_response_alg: RS256 issuer: ~ jwks_uri_override: ~ + on_backchannel_logout: do_nothing pkce_mode: auto response_mode: query scope: openid diff --git a/crates/syn2mas/src/mas_writer/syn2mas_revert_temporary_tables.sql b/crates/syn2mas/src/mas_writer/syn2mas_revert_temporary_tables.sql index e1df06f94..73f4cfe52 100644 --- a/crates/syn2mas/src/mas_writer/syn2mas_revert_temporary_tables.sql +++ b/crates/syn2mas/src/mas_writer/syn2mas_revert_temporary_tables.sql @@ -1,7 +1,7 @@ -- Copyright 2024 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. -- This script should revert what `syn2mas_temporary_tables.sql` does. diff --git a/crates/syn2mas/src/mas_writer/syn2mas_temporary_tables.sql b/crates/syn2mas/src/mas_writer/syn2mas_temporary_tables.sql index 9cda82881..873ceeb7e 100644 --- a/crates/syn2mas/src/mas_writer/syn2mas_temporary_tables.sql +++ b/crates/syn2mas/src/mas_writer/syn2mas_temporary_tables.sql @@ -1,7 +1,7 @@ -- Copyright 2024 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. -- # syn2mas Temporary Tables diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index 2a906d933..358470034 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! # Migration //! diff --git a/crates/syn2mas/src/progress.rs b/crates/syn2mas/src/progress.rs index 3c67825ce..cdd7ab417 100644 --- a/crates/syn2mas/src/progress.rs +++ b/crates/syn2mas/src/progress.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock, atomic::AtomicU32}; diff --git a/crates/syn2mas/src/synapse_reader/checks.rs b/crates/syn2mas/src/synapse_reader/checks.rs index a78f18b1d..655642770 100644 --- a/crates/syn2mas/src/synapse_reader/checks.rs +++ b/crates/syn2mas/src/synapse_reader/checks.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! # Synapse Checks //! @@ -25,7 +25,7 @@ pub enum Error { Sqlx(#[from] sqlx::Error), #[error("failed to load MAS config: {0}")] - MasConfig(#[from] figment::Error), + MasConfig(#[source] Box), #[error("failed to load MAS password config: {0}")] MasPasswordConfig(#[source] anyhow::Error), @@ -188,13 +188,13 @@ pub async fn synapse_config_check_against_mas_config( let mut errors = Vec::new(); let mut warnings = Vec::new(); - let mas_passwords = PasswordsConfig::extract_or_default(mas)?; + let mas_passwords = PasswordsConfig::extract_or_default(mas).map_err(Error::MasConfig)?; let mas_password_schemes = mas_passwords .load() .await .map_err(Error::MasPasswordConfig)?; - let mas_matrix = MatrixConfig::extract(mas)?; + let mas_matrix = MatrixConfig::extract(mas).map_err(Error::MasConfig)?; // Look for the MAS password hashing scheme that will be used for imported // Synapse passwords, then check the configuration matches so that Synapse @@ -230,12 +230,12 @@ pub async fn synapse_config_check_against_mas_config( }); } - let mas_captcha = CaptchaConfig::extract_or_default(mas)?; + let mas_captcha = CaptchaConfig::extract_or_default(mas).map_err(Error::MasConfig)?; if synapse.enable_registration_captcha && mas_captcha.service.is_none() { warnings.push(CheckWarning::ShouldPortRegistrationCaptcha); } - let mas_branding = BrandingConfig::extract_or_default(mas)?; + let mas_branding = BrandingConfig::extract_or_default(mas).map_err(Error::MasConfig)?; if synapse.user_consent.is_some() && mas_branding.tos_uri.is_none() { warnings.push(CheckWarning::ShouldPortUserConsentAsTerms); } @@ -295,7 +295,7 @@ pub async fn synapse_database_check( .await?; if !oauth_provider_user_counts.is_empty() { let syn_oauth2 = synapse.all_oidc_providers(); - let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas)?; + let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas).map_err(Error::MasConfig)?; for row in oauth_provider_user_counts { // This is a special case of a previous migration attempt to MAS if row.auth_provider == "oauth-delegated" { diff --git a/crates/syn2mas/src/synapse_reader/config/mod.rs b/crates/syn2mas/src/synapse_reader/config/mod.rs index 25a4c0d93..4bd89921b 100644 --- a/crates/syn2mas/src/synapse_reader/config/mod.rs +++ b/crates/syn2mas/src/synapse_reader/config/mod.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod oidc; @@ -94,7 +94,9 @@ impl Config { /// /// - If there is a problem reading any of the files. /// - If the configuration is not valid. - pub fn load(files: &[Utf8PathBuf]) -> Result { + pub fn load( + files: &[Utf8PathBuf], + ) -> Result> { let mut figment = figment::Figment::new(); for file in files { // TODO this is not exactly correct behaviour — Synapse does not merge anything @@ -103,7 +105,8 @@ impl Config { // https://github.com/element-hq/synapse/blob/develop/synapse/config/_base.py?rgh-link-date=2025-01-20T17%3A02%3A56Z#L870 figment = figment.merge(Yaml::file(file)); } - figment.extract::() + let config = figment.extract::()?; + Ok(config) } /// Returns a map of all OIDC providers from the Synapse configuration. diff --git a/crates/syn2mas/src/synapse_reader/config/oidc.rs b/crates/syn2mas/src/synapse_reader/config/oidc.rs index 9eea0a9be..49ee59a71 100644 --- a/crates/syn2mas/src/synapse_reader/config/oidc.rs +++ b/crates/syn2mas/src/synapse_reader/config/oidc.rs @@ -1,14 +1,15 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::BTreeMap, str::FromStr as _}; use chrono::{DateTime, Utc}; use mas_config::{ UpstreamOAuth2ClaimsImports, UpstreamOAuth2DiscoveryMode, UpstreamOAuth2ImportAction, - UpstreamOAuth2PkceMethod, UpstreamOAuth2ResponseMode, UpstreamOAuth2TokenAuthMethod, + UpstreamOAuth2OnBackchannelLogout, UpstreamOAuth2PkceMethod, UpstreamOAuth2ResponseMode, + UpstreamOAuth2TokenAuthMethod, }; use mas_iana::jose::JsonWebSignatureAlg; use oauth2_types::scope::{OPENID, Scope, ScopeToken}; @@ -159,7 +160,6 @@ pub struct OidcProvider { #[serde(default)] skip_verification: bool, - // Unsupported, we want to shout about it #[serde(default)] backchannel_logout_enabled: bool, @@ -193,7 +193,6 @@ impl OidcProvider { } /// Map this Synapse OIDC provider config to a MAS upstream provider config. - #[expect(clippy::too_many_lines)] pub(crate) fn into_mas_config( self, rng: &mut impl Rng, @@ -219,10 +218,6 @@ impl OidcProvider { warn!("The `id_token_signing_alg_values_supported` option is not supported, ignoring."); } - if self.backchannel_logout_enabled { - warn!("The `backchannel_logout_enabled` option is not supported, ignoring."); - } - if !self.enable_registration { warn!( "Setting the `enable_registration` option to `false` is not supported, ignoring." @@ -319,6 +314,12 @@ impl OidcProvider { self.user_mapping_provider.config.into_mas_config() }; + let on_backchannel_logout = if self.backchannel_logout_enabled { + UpstreamOAuth2OnBackchannelLogout::DoNothing + } else { + UpstreamOAuth2OnBackchannelLogout::LogoutBrowserOnly + }; + Some(mas_config::UpstreamOAuth2Provider { enabled: true, id, @@ -345,6 +346,7 @@ impl OidcProvider { claims_imports, additional_authorization_parameters, forward_login_hint: self.forward_login_hint, + on_backchannel_logout, }) } } diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql index e92fd21bf..4926445b9 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO access_tokens ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql index c8b2850ac..6029f94fe 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_puppet.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO access_tokens ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql index 180a58810..bba684b1a 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_refresh_token.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO access_tokens ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql index 8c7d1c695..e1de6b287 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/access_token_alice_with_unused_refresh_token.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO access_tokens ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql index 8eb50a3ba..411c6ba30 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/devices_alice.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO devices ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql index a365faf05..651f03cf9 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/external_ids_alice.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO user_external_ids ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql index 4bf680cce..5b643a462 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/threepids_alice.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO user_threepids ( diff --git a/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql b/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql index dc77d5859..825213402 100644 --- a/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql +++ b/crates/syn2mas/src/synapse_reader/fixtures/user_alice.sql @@ -1,7 +1,7 @@ -- Copyright 2024, 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. INSERT INTO users ( diff --git a/crates/syn2mas/src/synapse_reader/mod.rs b/crates/syn2mas/src/synapse_reader/mod.rs index 0b753e33b..b5b691495 100644 --- a/crates/syn2mas/src/synapse_reader/mod.rs +++ b/crates/syn2mas/src/synapse_reader/mod.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! # Synapse Database Reader //! diff --git a/crates/syn2mas/src/telemetry.rs b/crates/syn2mas/src/telemetry.rs index e9a3385fb..8d67d4bf4 100644 --- a/crates/syn2mas/src/telemetry.rs +++ b/crates/syn2mas/src/telemetry.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::LazyLock; diff --git a/crates/syn2mas/test_synapse_migrations/20250117064958_users.sql b/crates/syn2mas/test_synapse_migrations/20250117064958_users.sql index 5c67dc097..72cc46dab 100644 --- a/crates/syn2mas/test_synapse_migrations/20250117064958_users.sql +++ b/crates/syn2mas/test_synapse_migrations/20250117064958_users.sql @@ -1,26 +1,23 @@ -- Copyright 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. - +-- Please see LICENSE files in the repository root for full details. -- Brings in the `users` table from Synapse - CREATE TABLE users ( - name text, - password_hash text, - creation_ts bigint, - admin smallint DEFAULT 0 NOT NULL, - upgrade_ts bigint, - is_guest smallint DEFAULT 0 NOT NULL, - appservice_id text, - consent_version text, - consent_server_notice_sent text, - user_type text, - deactivated smallint DEFAULT 0 NOT NULL, - shadow_banned boolean, - consent_ts bigint, - approved boolean, - locked boolean DEFAULT false NOT NULL, - suspended boolean DEFAULT false NOT NULL + name text, + password_hash text, + creation_ts bigint, + admin smallint DEFAULT 0 NOT NULL, + upgrade_ts bigint, + is_guest smallint DEFAULT 0 NOT NULL, + appservice_id text, + consent_version text, + consent_server_notice_sent text, + user_type text, + deactivated smallint DEFAULT 0 NOT NULL, + shadow_banned boolean, + consent_ts bigint, + approved boolean, + locked boolean DEFAULT false NOT NULL, + suspended boolean DEFAULT false NOT NULL ); - diff --git a/crates/syn2mas/test_synapse_migrations/20250128141011_threepids.sql b/crates/syn2mas/test_synapse_migrations/20250128141011_threepids.sql index 2ff655979..3ee382b35 100644 --- a/crates/syn2mas/test_synapse_migrations/20250128141011_threepids.sql +++ b/crates/syn2mas/test_synapse_migrations/20250128141011_threepids.sql @@ -1,7 +1,7 @@ -- Copyright 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. -- Brings in the `user_threepids` table from Synapse diff --git a/crates/syn2mas/test_synapse_migrations/20250128162513_external_ids.sql b/crates/syn2mas/test_synapse_migrations/20250128162513_external_ids.sql index 09eec8430..9054accd3 100644 --- a/crates/syn2mas/test_synapse_migrations/20250128162513_external_ids.sql +++ b/crates/syn2mas/test_synapse_migrations/20250128162513_external_ids.sql @@ -1,7 +1,7 @@ -- Copyright 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. -- Brings in the `user_external_ids` table from Synapse diff --git a/crates/syn2mas/test_synapse_migrations/20250128201100_access_and_refresh_tokens.sql b/crates/syn2mas/test_synapse_migrations/20250128201100_access_and_refresh_tokens.sql index fef25bbbb..8b1ed58ea 100644 --- a/crates/syn2mas/test_synapse_migrations/20250128201100_access_and_refresh_tokens.sql +++ b/crates/syn2mas/test_synapse_migrations/20250128201100_access_and_refresh_tokens.sql @@ -1,7 +1,7 @@ -- Copyright 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. -- Brings in the `access_tokens` and `refresh_tokens` tables from Synapse diff --git a/crates/syn2mas/test_synapse_migrations/20250129140230_devices.sql b/crates/syn2mas/test_synapse_migrations/20250129140230_devices.sql index 8f9ae723b..129df1b68 100644 --- a/crates/syn2mas/test_synapse_migrations/20250129140230_devices.sql +++ b/crates/syn2mas/test_synapse_migrations/20250129140230_devices.sql @@ -1,7 +1,7 @@ -- Copyright 2025 New Vector Ltd. -- -- SPDX-License-Identifier: AGPL-3.0-only --- Please see LICENSE in the repository root for full details. +-- Please see LICENSE files in the repository root for full details. -- Brings in the `devices` table from Synapse CREATE TABLE devices ( diff --git a/crates/tasks/Cargo.toml b/crates/tasks/Cargo.toml index 9cd39c20b..5c2a31536 100644 --- a/crates/tasks/Cargo.toml +++ b/crates/tasks/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-tasks" version.workspace = true diff --git a/crates/tasks/src/database.rs b/crates/tasks/src/database.rs index fa424d7df..bc14215f8 100644 --- a/crates/tasks/src/database.rs +++ b/crates/tasks/src/database.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Database-related tasks @@ -24,7 +24,7 @@ impl RunnableJob for CleanupExpiredTokensJob { let count = repo .oauth2_access_token() - .cleanup_revoked(&clock) + .cleanup_revoked(clock) .await .map_err(JobError::retry)?; repo.save().await.map_err(JobError::retry)?; diff --git a/crates/tasks/src/email.rs b/crates/tasks/src/email.rs index 4eacdfaf6..8e685843a 100644 --- a/crates/tasks/src/email.rs +++ b/crates/tasks/src/email.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use async_trait::async_trait; use chrono::Duration; @@ -100,7 +100,7 @@ impl RunnableJob for SendEmailAuthenticationCodeJob { .user_email() .add_authentication_code( &mut rng, - &clock, + clock, Duration::minutes(5), // TODO: make this configurable &user_email_authentication, code, diff --git a/crates/tasks/src/lib.rs b/crates/tasks/src/lib.rs index cb1b16469..e2f539047 100644 --- a/crates/tasks/src/lib.rs +++ b/crates/tasks/src/lib.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::{Arc, LazyLock}; @@ -10,7 +10,7 @@ use mas_data_model::SiteConfig; use mas_email::Mailer; use mas_matrix::HomeserverConnection; use mas_router::UrlBuilder; -use mas_storage::{BoxClock, BoxRepository, RepositoryError, RepositoryFactory, SystemClock}; +use mas_storage::{BoxRepository, Clock, RepositoryError, RepositoryFactory}; use mas_storage_pg::PgRepositoryFactory; use new_queue::QueueRunnerError; use opentelemetry::metrics::Meter; @@ -18,6 +18,8 @@ use rand::SeedableRng; use sqlx::{Pool, Postgres}; use tokio_util::{sync::CancellationToken, task::TaskTracker}; +pub use crate::new_queue::QueueWorker; + mod database; mod email; mod matrix; @@ -39,7 +41,7 @@ static METER: LazyLock = LazyLock::new(|| { struct State { repository_factory: PgRepositoryFactory, mailer: Mailer, - clock: SystemClock, + clock: Arc, homeserver: Arc, url_builder: UrlBuilder, site_config: SiteConfig, @@ -48,7 +50,7 @@ struct State { impl State { pub fn new( repository_factory: PgRepositoryFactory, - clock: SystemClock, + clock: impl Clock + 'static, mailer: Mailer, homeserver: impl HomeserverConnection + 'static, url_builder: UrlBuilder, @@ -57,7 +59,7 @@ impl State { Self { repository_factory, mailer, - clock, + clock: Arc::new(clock), homeserver: Arc::new(homeserver), url_builder, site_config, @@ -68,8 +70,8 @@ impl State { self.repository_factory.pool() } - pub fn clock(&self) -> BoxClock { - Box::new(self.clock.clone()) + pub fn clock(&self) -> &dyn Clock { + &self.clock } pub fn mailer(&self) -> &Mailer { @@ -99,29 +101,31 @@ impl State { } } -/// Initialise the workers. +/// Initialise the worker, without running it. +/// +/// This is mostly useful for tests. /// /// # Errors /// /// This function can fail if the database connection fails. pub async fn init( repository_factory: PgRepositoryFactory, + clock: impl Clock + 'static, mailer: &Mailer, homeserver: impl HomeserverConnection + 'static, url_builder: UrlBuilder, site_config: &SiteConfig, cancellation_token: CancellationToken, - task_tracker: &TaskTracker, -) -> Result<(), QueueRunnerError> { +) -> Result { let state = State::new( repository_factory, - SystemClock::default(), + clock, mailer.clone(), homeserver, url_builder, site_config.clone(), ); - let mut worker = self::new_queue::QueueWorker::new(state, cancellation_token).await?; + let mut worker = QueueWorker::new(state, cancellation_token).await?; worker .register_handler::() @@ -157,6 +161,36 @@ pub async fn init( mas_storage::queue::PruneStalePolicyDataJob, ); + Ok(worker) +} + +/// Initialise the worker and run it. +/// +/// # Errors +/// +/// This function can fail if the database connection fails. +#[expect(clippy::too_many_arguments, reason = "this is fine")] +pub async fn init_and_run( + repository_factory: PgRepositoryFactory, + clock: impl Clock + 'static, + mailer: &Mailer, + homeserver: impl HomeserverConnection + 'static, + url_builder: UrlBuilder, + site_config: &SiteConfig, + cancellation_token: CancellationToken, + task_tracker: &TaskTracker, +) -> Result<(), QueueRunnerError> { + let worker = init( + repository_factory, + clock, + mailer, + homeserver, + url_builder, + site_config, + cancellation_token, + ) + .await?; + task_tracker.spawn(worker.run()); Ok(()) diff --git a/crates/tasks/src/matrix.rs b/crates/tasks/src/matrix.rs index 3060b3d7b..d5d6e6e65 100644 --- a/crates/tasks/src/matrix.rs +++ b/crates/tasks/src/matrix.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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; @@ -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 { @@ -80,7 +81,7 @@ impl RunnableJob for ProvisionUserJob { // Schedule a device sync job let sync_device_job = SyncDevicesJob::new(&user); repo.queue_job() - .schedule_job(&mut rng, &clock, sync_device_job) + .schedule_job(&mut rng, clock, sync_device_job) .await .map_err(JobError::retry)?; @@ -118,7 +119,7 @@ impl RunnableJob for ProvisionDeviceJob { // Schedule a device sync job repo.queue_job() - .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user)) + .schedule_job(&mut rng, clock, SyncDevicesJob::new(&user)) .await .map_err(JobError::retry)?; @@ -154,7 +155,7 @@ impl RunnableJob for DeleteDeviceJob { // Schedule a device sync job repo.queue_job() - .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user)) + .schedule_job(&mut rng, clock, SyncDevicesJob::new(&user)) .await .map_err(JobError::retry)?; @@ -191,7 +192,7 @@ impl RunnableJob for SyncDevicesJob { let mut devices = HashSet::new(); // Cycle through all the compat sessions of the user, and grab the devices - let mut cursor = Pagination::first(100); + let mut cursor = Pagination::first(5000); loop { let page = repo .compat_session() @@ -215,7 +216,7 @@ impl RunnableJob for SyncDevicesJob { } // Cycle though all the oauth2 sessions of the user, and grab the devices - let mut cursor = Pagination::first(100); + let mut cursor = Pagination::first(5000); loop { let page = repo .oauth2_session() @@ -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/crates/tasks/src/new_queue.rs b/crates/tasks/src/new_queue.rs index 1c83b1720..06f358f31 100644 --- a/crates/tasks/src/new_queue.rs +++ b/crates/tasks/src/new_queue.rs @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, sync::Arc}; @@ -19,7 +19,6 @@ use opentelemetry::{ metrics::{Counter, Histogram, UpDownCounter}, }; use rand::{Rng, RngCore, distributions::Uniform}; -use rand_chacha::ChaChaRng; use serde::de::DeserializeOwned; use sqlx::{ Acquire, Either, @@ -195,8 +194,6 @@ struct ScheduleDefinition { } pub struct QueueWorker { - rng: ChaChaRng, - clock: Box, listener: PgListener, registration: Worker, am_i_leader: bool, @@ -217,7 +214,7 @@ impl QueueWorker { skip_all, fields(worker.id) )] - pub async fn new( + pub(crate) async fn new( state: State, cancellation_token: CancellationToken, ) -> Result { @@ -246,7 +243,7 @@ impl QueueWorker { .map_err(QueueRunnerError::StartTransaction)?; let mut repo = PgRepository::from_conn(txn); - let registration = repo.queue_worker().register(&mut rng, &clock).await?; + let registration = repo.queue_worker().register(&mut rng, clock).await?; tracing::Span::current().record("worker.id", tracing::field::display(registration.id)); repo.into_inner() .commit() @@ -278,8 +275,6 @@ impl QueueWorker { let cancellation_guard = cancellation_token.clone().drop_guard(); Ok(Self { - rng, - clock, listener, registration, am_i_leader: false, @@ -294,7 +289,7 @@ impl QueueWorker { }) } - pub fn register_handler(&mut self) -> &mut Self { + pub(crate) fn register_handler(&mut self) -> &mut Self { // There is a potential panic here, which is fine as it's going to be caught // within the job task let factory = |payload: JobPayload| { @@ -307,7 +302,7 @@ impl QueueWorker { self } - pub fn add_schedule( + pub(crate) fn add_schedule( &mut self, schedule_name: &'static str, expression: Schedule, @@ -325,7 +320,7 @@ impl QueueWorker { self } - pub async fn run(mut self) { + pub(crate) async fn run(mut self) { if let Err(e) = self.run_inner().await { tracing::error!( error = &e as &dyn std::error::Error, @@ -349,7 +344,7 @@ impl QueueWorker { } #[tracing::instrument(name = "worker.setup_schedules", skip_all)] - pub async fn setup_schedules(&mut self) -> Result<(), QueueRunnerError> { + pub(crate) async fn setup_schedules(&mut self) -> Result<(), QueueRunnerError> { let schedules: Vec<_> = self.schedules.iter().map(|s| s.schedule_name).collect(); // Start a transaction on the existing PgListener connection @@ -397,6 +392,9 @@ impl QueueWorker { async fn shutdown(&mut self) -> Result<(), QueueRunnerError> { tracing::info!("Shutting down worker"); + let clock = self.state.clock(); + let mut rng = self.state.rng(); + // Start a transaction on the existing PgListener connection let txn = self .listener @@ -421,13 +419,13 @@ impl QueueWorker { // Wait for all the jobs to finish self.tracker - .process_jobs(&mut self.rng, &self.clock, &mut repo, true) + .process_jobs(&mut rng, clock, &mut repo, true) .await?; // Tell the other workers we're shutting down // This also releases the leader election lease repo.queue_worker() - .shutdown(&self.clock, &self.registration) + .shutdown(clock, &self.registration) .await?; repo.into_inner() @@ -440,12 +438,12 @@ impl QueueWorker { #[tracing::instrument(name = "worker.wait_until_wakeup", skip_all)] async fn wait_until_wakeup(&mut self) -> Result<(), QueueRunnerError> { + let mut rng = self.state.rng(); + // This is to make sure we wake up every second to do the maintenance tasks // We add a little bit of random jitter to the duration, so that we don't get // fully synced workers waking up at the same time after each notification - let sleep_duration = self - .rng - .sample(Uniform::new(MIN_SLEEP_DURATION, MAX_SLEEP_DURATION)); + let sleep_duration = rng.sample(Uniform::new(MIN_SLEEP_DURATION, MAX_SLEEP_DURATION)); let wakeup_sleep = tokio::time::sleep(sleep_duration); tokio::select! { @@ -490,7 +488,9 @@ impl QueueWorker { )] async fn tick(&mut self) -> Result<(), QueueRunnerError> { tracing::debug!("Tick"); - let now = self.clock.now(); + let clock = self.state.clock(); + let mut rng = self.state.rng(); + let now = clock.now(); // Start a transaction on the existing PgListener connection let txn = self @@ -505,25 +505,25 @@ impl QueueWorker { if now - self.last_heartbeat >= chrono::Duration::minutes(1) { tracing::info!("Sending heartbeat"); repo.queue_worker() - .heartbeat(&self.clock, &self.registration) + .heartbeat(clock, &self.registration) .await?; self.last_heartbeat = now; } // Remove any dead worker leader leases repo.queue_worker() - .remove_leader_lease_if_expired(&self.clock) + .remove_leader_lease_if_expired(clock) .await?; // Try to become (or stay) the leader let leader = repo .queue_worker() - .try_get_leader_lease(&self.clock, &self.registration) + .try_get_leader_lease(clock, &self.registration) .await?; // Process any job task which finished self.tracker - .process_jobs(&mut self.rng, &self.clock, &mut repo, false) + .process_jobs(&mut rng, clock, &mut repo, false) .await?; // Compute how many jobs we should fetch at most @@ -538,7 +538,7 @@ impl QueueWorker { let queues = self.tracker.queues(); let jobs = repo .queue_job() - .reserve(&self.clock, &self.registration, &queues, max_jobs_to_fetch) + .reserve(clock, &self.registration, &queues, max_jobs_to_fetch) .await?; for Job { @@ -592,6 +592,9 @@ impl QueueWorker { return Err(QueueRunnerError::NotLeader); } + let clock = self.state.clock(); + let mut rng = self.state.rng(); + // Start a transaction on the existing PgListener connection let txn = self .listener @@ -633,10 +636,10 @@ impl QueueWorker { // Look at the state of schedules in the database let schedules_status = repo.queue_schedule().list().await?; - let now = self.clock.now(); + let now = clock.now(); for schedule in &self.schedules { // Find the schedule status from the database - let Some(schedule_status) = schedules_status + let Some(status) = schedules_status .iter() .find(|s| s.schedule_name == schedule.schedule_name) else { @@ -648,13 +651,13 @@ impl QueueWorker { }; // Figure out if we should schedule a new job - if let Some(next_time) = schedule_status.last_scheduled_at { + if let Some(next_time) = status.last_scheduled_at { if next_time > now { // We already have a job scheduled in the future, skip continue; } - if schedule_status.last_scheduled_job_completed == Some(false) { + if status.last_scheduled_job_completed == Some(false) { // The last scheduled job has not completed yet, skip continue; } @@ -670,8 +673,8 @@ impl QueueWorker { repo.queue_job() .schedule_later( - &mut self.rng, - &self.clock, + &mut rng, + clock, schedule.queue_name, schedule.payload.clone(), serde_json::json!({}), @@ -684,16 +687,13 @@ impl QueueWorker { // We also check if the worker is dead, and if so, we shutdown all the dead // workers that haven't checked in the last two minutes repo.queue_worker() - .shutdown_dead_workers(&self.clock, Duration::minutes(2)) + .shutdown_dead_workers(clock, Duration::minutes(2)) .await?; // TODO: mark tasks those workers had as lost // Mark all the scheduled jobs as available - let scheduled = repo - .queue_job() - .schedule_available_jobs(&self.clock) - .await?; + let scheduled = repo.queue_job().schedule_available_jobs(clock).await?; match scheduled { 0 => {} 1 => tracing::info!("One scheduled job marked as available"), @@ -713,6 +713,73 @@ impl QueueWorker { Ok(()) } + + /// Process all the pending jobs in the queue. + /// This should only be called in tests! + /// + /// # Errors + /// + /// This function can fail if the database connection fails. + pub async fn process_all_jobs_in_tests(&mut self) -> Result<(), QueueRunnerError> { + // I swear, I'm the leader! + self.am_i_leader = true; + + // First, perform the leader duties. This will make sure that we schedule + // recurring jobs. + self.perform_leader_duties().await?; + + let clock = self.state.clock(); + let mut rng = self.state.rng(); + + // Grab the connection from the PgListener + let txn = self + .listener + .begin() + .await + .map_err(QueueRunnerError::StartTransaction)?; + let mut repo = PgRepository::from_conn(txn); + + // Spawn all the jobs in the database + let queues = self.tracker.queues(); + let jobs = repo + .queue_job() + // I really hope that we don't spawn more than 10k jobs in tests + .reserve(clock, &self.registration, &queues, 10_000) + .await?; + + for Job { + id, + queue_name, + payload, + metadata, + attempt, + } in jobs + { + let cancellation_token = self.cancellation_token.child_token(); + let start = Instant::now(); + let context = JobContext { + id, + metadata, + queue_name, + attempt, + start, + cancellation_token, + }; + + self.tracker.spawn_job(self.state.clone(), context, payload); + } + + self.tracker + .process_jobs(&mut rng, clock, &mut repo, true) + .await?; + + repo.into_inner() + .commit() + .await + .map_err(QueueRunnerError::CommitTransaction)?; + + Ok(()) + } } /// Tracks running jobs @@ -893,7 +960,6 @@ impl JobTracker { /// If `blocking` is `true`, this function will block until all the jobs /// are finished. Otherwise, it will return as soon as it processed the /// already finished jobs. - #[allow(clippy::too_many_lines)] async fn process_jobs( &mut self, rng: &mut (dyn RngCore + Send), diff --git a/crates/tasks/src/recovery.rs b/crates/tasks/src/recovery.rs index 9d68dad66..03e02d57b 100644 --- a/crates/tasks/src/recovery.rs +++ b/crates/tasks/src/recovery.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context; use async_trait::async_trait; @@ -75,7 +75,7 @@ impl RunnableJob for SendAccountRecoveryEmailsJob { let ticket = repo .user_recovery() - .add_ticket(&mut rng, &clock, &session, &email, ticket) + .add_ticket(&mut rng, clock, &session, &email, ticket) .await .map_err(JobError::retry)?; diff --git a/crates/tasks/src/sessions.rs b/crates/tasks/src/sessions.rs index fc7f89796..d10d908da 100644 --- a/crates/tasks/src/sessions.rs +++ b/crates/tasks/src/sessions.rs @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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; @@ -39,7 +39,7 @@ impl RunnableJob for ExpireInactiveSessionsJob { repo.queue_job() .schedule_job( &mut rng, - &clock, + clock, ExpireInactiveOAuthSessionsJob::new(now - ttl), ) .await @@ -50,7 +50,7 @@ impl RunnableJob for ExpireInactiveSessionsJob { repo.queue_job() .schedule_job( &mut rng, - &clock, + clock, ExpireInactiveCompatSessionsJob::new(now - ttl), ) .await @@ -61,7 +61,7 @@ impl RunnableJob for ExpireInactiveSessionsJob { repo.queue_job() .schedule_job( &mut rng, - &clock, + clock, ExpireInactiveUserSessionsJob::new(now - ttl), ) .await @@ -104,7 +104,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { if let Some(job) = self.next(&page) { tracing::info!("Scheduling job to expire the next batch of inactive sessions"); repo.queue_job() - .schedule_job(&mut rng, &clock, job) + .schedule_job(&mut rng, clock, job) .await .map_err(JobError::retry)?; } @@ -117,7 +117,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { repo.queue_job() .schedule_job_later( &mut rng, - &clock, + clock, SyncDevicesJob::new_for_id(user_id), clock.now() + delay, ) @@ -128,7 +128,7 @@ impl RunnableJob for ExpireInactiveOAuthSessionsJob { } repo.oauth2_session() - .finish(&clock, edge) + .finish(clock, edge) .await .map_err(JobError::retry)?; } @@ -168,7 +168,7 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { if let Some(job) = self.next(&page) { tracing::info!("Scheduling job to expire the next batch of inactive sessions"); repo.queue_job() - .schedule_job(&mut rng, &clock, job) + .schedule_job(&mut rng, clock, job) .await .map_err(JobError::retry)?; } @@ -180,7 +180,7 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { repo.queue_job() .schedule_job_later( &mut rng, - &clock, + clock, SyncDevicesJob::new_for_id(edge.user_id), clock.now() + delay, ) @@ -190,7 +190,7 @@ impl RunnableJob for ExpireInactiveCompatSessionsJob { } repo.compat_session() - .finish(&clock, edge) + .finish(clock, edge) .await .map_err(JobError::retry)?; } @@ -223,14 +223,14 @@ impl RunnableJob for ExpireInactiveUserSessionsJob { if let Some(job) = self.next(&page) { tracing::info!("Scheduling job to expire the next batch of inactive sessions"); repo.queue_job() - .schedule_job(&mut rng, &clock, job) + .schedule_job(&mut rng, clock, job) .await .map_err(JobError::retry)?; } for edge in page.edges { repo.browser_session() - .finish(&clock, edge) + .finish(clock, edge) .await .map_err(JobError::retry)?; } diff --git a/crates/tasks/src/user.rs b/crates/tasks/src/user.rs index 4dfa081c4..ee60c6532 100644 --- a/crates/tasks/src/user.rs +++ b/crates/tasks/src/user.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use anyhow::Context; use async_trait::async_trait; @@ -41,17 +41,10 @@ impl RunnableJob for DeactivateUserJob { .context("User not found") .map_err(JobError::fail)?; - // Let's first lock & deactivate the user + // Let's first deactivate the user let user = repo .user() - .lock(&clock, user) - .await - .context("Failed to lock user") - .map_err(JobError::retry)?; - - let user = repo - .user() - .deactivate(&clock, user) + .deactivate(clock, user) .await .context("Failed to deactivate user") .map_err(JobError::retry)?; @@ -60,7 +53,7 @@ impl RunnableJob for DeactivateUserJob { let n = repo .browser_session() .finish_bulk( - &clock, + clock, BrowserSessionFilter::new().for_user(&user).active_only(), ) .await @@ -70,7 +63,7 @@ impl RunnableJob for DeactivateUserJob { let n = repo .oauth2_session() .finish_bulk( - &clock, + clock, OAuth2SessionFilter::new().for_user(&user).active_only(), ) .await @@ -80,7 +73,7 @@ impl RunnableJob for DeactivateUserJob { let n = repo .compat_session() .finish_bulk( - &clock, + clock, CompatSessionFilter::new().for_user(&user).active_only(), ) .await @@ -99,10 +92,9 @@ impl RunnableJob for DeactivateUserJob { // we want the user to be locked out as soon as possible repo.save().await.map_err(JobError::retry)?; - let mxid = matrix.mxid(&user.username); - info!("Deactivating user {} on homeserver", mxid); + info!("Deactivating user {} on homeserver", user.username); matrix - .delete_user(&mxid, self.hs_erase()) + .delete_user(&user.username, self.hs_erase()) .await .map_err(JobError::retry)?; @@ -130,16 +122,19 @@ impl RunnableJob for ReactivateUserJob { .context("User not found") .map_err(JobError::fail)?; - let mxid = matrix.mxid(&user.username); - info!("Reactivating user {} on homeserver", mxid); + info!("Reactivating user {} on homeserver", user.username); matrix - .reactivate_user(&mxid) + .reactivate_user(&user.username) .await .map_err(JobError::retry)?; - // We want to unlock the user from our side only once it has been reactivated on - // the homeserver - let _user = repo.user().unlock(user).await.map_err(JobError::retry)?; + // We want to reactivate the user from our side only once it has been + // reactivated on the homeserver + let _user = repo + .user() + .reactivate(user) + .await + .map_err(JobError::retry)?; repo.save().await.map_err(JobError::retry)?; Ok(()) diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index a99f5ae56..ffe2dbf74 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-templates" version.workspace = true diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index c21096ee6..230dee67f 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. //! Contexts used in templates @@ -21,9 +21,9 @@ use http::{Method, Uri, Version}; use mas_data_model::{ AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState, DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports, - UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode, - UpstreamOAuthProviderTokenAuthMethod, User, UserEmailAuthentication, - UserEmailAuthenticationCode, UserRecoverySession, UserRegistration, + UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderOnBackchannelLogout, + UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod, User, + UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, UserRegistration, }; use mas_i18n::DataLocale; use mas_iana::jose::JsonWebSignatureAlg; @@ -1332,7 +1332,7 @@ impl TemplateContext for RecoveryFinishContext { } } -/// Context used by the `pages/upstream_oauth2/{link_mismatch,do_login}.html` +/// Context used by the `pages/upstream_oauth2/{link_mismatch,login_link}.html` /// templates #[derive(Serialize)] pub struct UpstreamExistingLinkContext { @@ -1543,6 +1543,7 @@ impl TemplateContext for UpstreamRegister { forward_login_hint: false, created_at: now, disabled_at: None, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, }, )] } diff --git a/crates/templates/src/context/branding.rs b/crates/templates/src/context/branding.rs index d9a451c4d..eb7e3546a 100644 --- a/crates/templates/src/context/branding.rs +++ b/crates/templates/src/context/branding.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; diff --git a/crates/templates/src/context/captcha.rs b/crates/templates/src/context/captcha.rs index 38d723ca0..442cea4f8 100644 --- a/crates/templates/src/context/captcha.rs +++ b/crates/templates/src/context/captcha.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; diff --git a/crates/templates/src/context/ext.rs b/crates/templates/src/context/ext.rs index 5a94430a7..e4ae3886c 100644 --- a/crates/templates/src/context/ext.rs +++ b/crates/templates/src/context/ext.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use mas_data_model::SiteConfig; diff --git a/crates/templates/src/context/features.rs b/crates/templates/src/context/features.rs index a493e0cee..d514b5c63 100644 --- a/crates/templates/src/context/features.rs +++ b/crates/templates/src/context/features.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::sync::Arc; diff --git a/crates/templates/src/forms.rs b/crates/templates/src/forms.rs index 2b769e122..94e20be3a 100644 --- a/crates/templates/src/forms.rs +++ b/crates/templates/src/forms.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// 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::HashMap, hash::Hash}; diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index 3229cde28..631e4742e 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // This is needed to make the Environment::add* functions work #![allow(clippy::needless_pass_by_value)] diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 88a72225a..d588e12ea 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![deny(missing_docs)] #![allow(clippy::module_name_repetitions)] @@ -135,6 +135,10 @@ fn is_hidden(entry: &DirEntry) -> bool { impl Templates { /// Load the templates from the given config + /// + /// # Errors + /// + /// Returns an error if the templates could not be loaded from disk. #[tracing::instrument( name = "templates.load", skip_all, @@ -254,6 +258,10 @@ impl Templates { } /// Reload the templates on disk + /// + /// # Errors + /// + /// Returns an error if the templates could not be reloaded from disk. #[tracing::instrument( name = "templates.reload", skip_all, @@ -401,6 +409,9 @@ register_templates! { /// Render the upstream link mismatch message pub fn render_upstream_oauth2_link_mismatch(WithLanguage>>) { "pages/upstream_oauth2/link_mismatch.html" } + /// Render the upstream link match + pub fn render_upstream_oauth2_login_link(WithLanguage>) { "pages/upstream_oauth2/login_link.html" } + /// Render the upstream suggest link message pub fn render_upstream_oauth2_suggest_link(WithLanguage>>) { "pages/upstream_oauth2/suggest_link.html" } @@ -468,6 +479,7 @@ impl Templates { check::render_email_verification_html(self, now, rng)?; check::render_email_verification_subject(self, now, rng)?; check::render_upstream_oauth2_link_mismatch(self, now, rng)?; + check::render_upstream_oauth2_login_link(self, now, rng)?; check::render_upstream_oauth2_suggest_link(self, now, rng)?; check::render_upstream_oauth2_do_register(self, now, rng)?; check::render_device_link(self, now, rng)?; diff --git a/crates/templates/src/macros.rs b/crates/templates/src/macros.rs index a3166b2bb..e0b203735 100644 --- a/crates/templates/src/macros.rs +++ b/crates/templates/src/macros.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2021-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. /// Count the number of tokens. Used to have a fixed-sized array for the /// templates list. diff --git a/crates/tower/Cargo.toml b/crates/tower/Cargo.toml index 38067f120..44a17c670 100644 --- a/crates/tower/Cargo.toml +++ b/crates/tower/Cargo.toml @@ -1,3 +1,8 @@ +# 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. + [package] name = "mas-tower" description = "Tower layers used by the Matrix Authentication Service" diff --git a/crates/tower/src/lib.rs b/crates/tower/src/lib.rs index 7c3bd077b..72fd97c9e 100644 --- a/crates/tower/src/lib.rs +++ b/crates/tower/src/lib.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. #![allow(clippy::module_name_repetitions)] diff --git a/crates/tower/src/metrics/duration.rs b/crates/tower/src/metrics/duration.rs index e0664fb31..d8f3cc52f 100644 --- a/crates/tower/src/metrics/duration.rs +++ b/crates/tower/src/metrics/duration.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::time::Instant; diff --git a/crates/tower/src/metrics/in_flight.rs b/crates/tower/src/metrics/in_flight.rs index 8f362c6cb..02a21540f 100644 --- a/crates/tower/src/metrics/in_flight.rs +++ b/crates/tower/src/metrics/in_flight.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use opentelemetry::{KeyValue, metrics::UpDownCounter}; use pin_project_lite::pin_project; diff --git a/crates/tower/src/metrics/make_attributes.rs b/crates/tower/src/metrics/make_attributes.rs index a6e38378d..e4bc3ca68 100644 --- a/crates/tower/src/metrics/make_attributes.rs +++ b/crates/tower/src/metrics/make_attributes.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use opentelemetry::{KeyValue, Value}; diff --git a/crates/tower/src/metrics/mod.rs b/crates/tower/src/metrics/mod.rs index da919af01..9b9094c77 100644 --- a/crates/tower/src/metrics/mod.rs +++ b/crates/tower/src/metrics/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod duration; mod in_flight; diff --git a/crates/tower/src/trace_context.rs b/crates/tower/src/trace_context.rs index 3621490c5..9180201c6 100644 --- a/crates/tower/src/trace_context.rs +++ b/crates/tower/src/trace_context.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use http::Request; use opentelemetry::propagation::Injector; diff --git a/crates/tower/src/tracing/enrich_span.rs b/crates/tower/src/tracing/enrich_span.rs index 9d2b83c84..1c726ba3b 100644 --- a/crates/tower/src/tracing/enrich_span.rs +++ b/crates/tower/src/tracing/enrich_span.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use tracing::{Span, Value}; diff --git a/crates/tower/src/tracing/future.rs b/crates/tower/src/tracing/future.rs index aeec33f39..bac624281 100644 --- a/crates/tower/src/tracing/future.rs +++ b/crates/tower/src/tracing/future.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use std::task::ready; diff --git a/crates/tower/src/tracing/layer.rs b/crates/tower/src/tracing/layer.rs index 70ad27954..1da80e90d 100644 --- a/crates/tower/src/tracing/layer.rs +++ b/crates/tower/src/tracing/layer.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use tower::Layer; use tracing::Span; diff --git a/crates/tower/src/tracing/make_span.rs b/crates/tower/src/tracing/make_span.rs index f84cab73e..ba03f47e6 100644 --- a/crates/tower/src/tracing/make_span.rs +++ b/crates/tower/src/tracing/make_span.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use tracing::Span; diff --git a/crates/tower/src/tracing/mod.rs b/crates/tower/src/tracing/mod.rs index bcd34fad8..0c355e654 100644 --- a/crates/tower/src/tracing/mod.rs +++ b/crates/tower/src/tracing/mod.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. mod enrich_span; mod future; diff --git a/crates/tower/src/tracing/service.rs b/crates/tower/src/tracing/service.rs index 8a292ad83..9c3484519 100644 --- a/crates/tower/src/tracing/service.rs +++ b/crates/tower/src/tracing/service.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use tower::Service; diff --git a/crates/tower/src/utils.rs b/crates/tower/src/utils.rs index b0c7a5b18..c9a6e9e4f 100644 --- a/crates/tower/src/utils.rs +++ b/crates/tower/src/utils.rs @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. use opentelemetry::{KeyValue, Value}; use tower::{Layer, Service}; diff --git a/deny.toml b/deny.toml index 152c525e5..bfc125dcf 100644 --- a/deny.toml +++ b/deny.toml @@ -1,3 +1,8 @@ +# 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. + [graph] targets = [ { triple = "x86_64-unknown-linux-gnu" }, @@ -14,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] @@ -35,6 +36,7 @@ allow = [ "OpenSSL", "Unicode-3.0", "Zlib", + "CDLA-Permissive-2.0", ] # Ring's license is a bit complicated, so we need to specify it manually @@ -68,11 +70,18 @@ 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 + # cron depends on this old version + # https://github.com/zslayton/cron/pull/137 + { name = "winnow", version = "0.6.20" }, + # 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" }, + + # This is a compatibility version of webpki-roots that depends on the 1.0 version + { name = "webpki-roots", version = "0.26.11" }, # We are still mainly using rand 0.8 { name = "rand", version = "0.8.5" }, @@ -92,3 +101,8 @@ deny = ["oldtime"] unknown-registry = "warn" unknown-git = "warn" allow-registry = ["https://github.com/rust-lang/crates.io-index"] + +allow-git = [ + # https://github.com/open-telemetry/opentelemetry-rust/pull/3076 + "https://github.com/sandhose/opentelemetry-rust", +] diff --git a/docker-bake.hcl b/docker-bake.hcl index cfa6b8353..3c3cac3af 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,3 +1,8 @@ +# 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. +# // This is used to set the version reported by the binary through an environment // variable. This is mainly useful when building out of a git context, like in // CI, where we don't have the full commit history available diff --git a/docs/api/spec.json b/docs/api/spec.json index 49dd7b0dd..9e3e336aa 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -1345,7 +1345,7 @@ "user" ], "summary": "Deactivate a user", - "description": "Calling this endpoint will lock and deactivate the user, preventing them from doing any action.\nThis invalidates any existing session, and will ask the homeserver to make them leave all rooms.", + "description": "Calling this endpoint will deactivate the user, preventing them from doing any action.\nThis invalidates any existing session, and will ask the homeserver to make them leave all rooms.", "operationId": "deactivateUser", "parameters": [ { @@ -1359,6 +1359,15 @@ "style": "simple" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivateUserRequest" + } + } + } + }, "responses": { "200": { "description": "User was deactivated", @@ -1409,6 +1418,76 @@ } } }, + "/api/admin/v1/users/{id}/reactivate": { + "post": { + "tags": [ + "user" + ], + "summary": "Reactivate a user", + "description": "Calling this endpoint will reactivate a deactivated user.\nThis DOES NOT unlock a locked user, which is still prevented from doing any action until it is explicitly unlocked.", + "operationId": "reactivateUser", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "title": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "User was reactivated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleResponse_for_User" + }, + "example": { + "data": { + "type": "user", + "id": "01040G2081040G2081040G2081", + "attributes": { + "username": "alice", + "created_at": "1970-01-01T00:00:00Z", + "locked_at": null, + "deactivated_at": null, + "admin": false + }, + "links": { + "self": "/api/admin/v1/users/01040G2081040G2081040G2081" + } + }, + "links": { + "self": "/api/admin/v1/users/01040G2081040G2081040G2081/reactivate" + } + } + } + } + }, + "404": { + "description": "User ID not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "errors": [ + { + "title": "User ID 00000000000000000000000000 not found" + } + ] + } + } + } + } + } + } + }, "/api/admin/v1/users/{id}/lock": { "post": { "tags": [ @@ -1485,6 +1564,7 @@ "user" ], "summary": "Unlock a user", + "description": "Calling this endpoint will lift restrictions on user actions that had imposed by locking.\nThis DOES NOT reactivate a deactivated user, which will remain unavailable until it is explicitly reactivated.", "operationId": "unlockUser", "parameters": [ { @@ -3872,6 +3952,17 @@ } } }, + "DeactivateUserRequest": { + "title": "JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint", + "type": "object", + "properties": { + "skip_erase": { + "description": "Whether to skip requesting the homeserver to GDPR-erase the user upon deactivation.", + "default": false, + "type": "boolean" + } + } + }, "UserEmailFilter": { "type": "object", "properties": { diff --git a/docs/config.schema.json b/docs/config.schema.json index 534165920..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" + ] } ] }, @@ -2136,6 +2150,14 @@ "description": "Whether the `login_hint` should be forwarded to the provider in the authorization request.\n\nDefaults to `false`.", "default": false, "type": "boolean" + }, + "on_backchannel_logout": { + "description": "What to do when receiving an OIDC Backchannel logout request.\n\nDefaults to \"do_nothing\".", + "allOf": [ + { + "$ref": "#/definitions/OnBackchannelLogout" + } + ] } } }, @@ -2353,6 +2375,14 @@ "template": { "description": "The Jinja2 template to use for the localpart attribute\n\nIf not provided, the default template is `{{ user.preferred_username }}`", "type": "string" + }, + "on_conflict": { + "description": "How to handle conflicts on the claim, default value is `Fail`", + "allOf": [ + { + "$ref": "#/definitions/OnConflict" + } + ] } } }, @@ -2389,6 +2419,25 @@ } ] }, + "OnConflict": { + "description": "How to handle an existing localpart claim", + "oneOf": [ + { + "description": "Fails the sso login on conflict", + "type": "string", + "enum": [ + "fail" + ] + }, + { + "description": "Adds the oauth identity link, regardless of whether there is an existing link or not", + "type": "string", + "enum": [ + "add" + ] + } + ] + }, "DisplaynameImportPreference": { "description": "What should be done for the displayname attribute", "type": "object", @@ -2435,6 +2484,32 @@ } } }, + "OnBackchannelLogout": { + "description": "What to do when receiving an OIDC Backchannel logout request.", + "oneOf": [ + { + "description": "Do nothing", + "type": "string", + "enum": [ + "do_nothing" + ] + }, + { + "description": "Only log out the MAS 'browser session' started by this OIDC session", + "type": "string", + "enum": [ + "logout_browser_only" + ] + }, + { + "description": "Log out all sessions started by this OIDC session, including MAS 'browser sessions' and client sessions", + "type": "string", + "enum": [ + "logout_all" + ] + } + ] + }, "BrandingConfig": { "description": "Configuration section for tweaking the branding of the service", "type": "object", diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 83edcfba2..abf91c2e4 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -4,7 +4,7 @@ This document aims to get you started with contributing to the Matrix Authentica ## 1. Who can contribute to MAS? -Everyone is welcome to contribute code to [Synapse](https://github.com/element-hq/matrix-authentication-service), provided that they are willing to license their contributions to Element under a [Contributor License Agreement](https://cla-assistant.io/element-hq/matrix-authentication-service) (CLA). This ensures that their contribution will be made available under an OSI-approved open-source license, currently Affero General Public License v3 (AGPLv3). +Everyone is welcome to contribute code to [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service), provided that they are willing to license their contributions to Element under a [Contributor License Agreement](https://cla-assistant.io/element-hq/matrix-authentication-service) (CLA). This ensures that their contribution will be made available under an OSI-approved open-source license, currently Affero General Public License v3 (AGPLv3). Please see the [Element blog post](https://element.io/blog/synapse-now-lives-at-github-com-element-hq-synapse/) for the full rationale. diff --git a/docs/development/releasing.md b/docs/development/releasing.md index 723815437..510a7e323 100644 --- a/docs/development/releasing.md +++ b/docs/development/releasing.md @@ -117,7 +117,7 @@ At this point, the releaser should check the changelog and ensure the "Set as pr [`translations-download` workflow]: https://github.com/element-hq/matrix-authentication-service/actions/workflows/translations-download.yaml [`release-branch` workflow]: https://github.com/element-hq/matrix-authentication-service/actions/workflows/release-branch.yaml [`release-bump` workflow]: https://github.com/element-hq/matrix-authentication-service/actions/workflows/release-bump.yaml -[`build` workflow]: https://github.com/element-hq/matrix-authentication-service/actions/workflows/build +[`build` workflow]: https://github.com/element-hq/matrix-authentication-service/actions/workflows/build.yaml [translation download PR]: https://github.com/element-hq/matrix-authentication-service/pulls?q=is%3Apr+label%3AA-I18n [CI to churn]: https://github.com/element-hq/matrix-authentication-service/actions/workflows/build.yaml?query=event%3Apush+actor%3Amatrixbot [draft release to appear]: https://github.com/element-hq/matrix-authentication-service/releases diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 3d4f5c4ae..79b2b29ee 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -257,8 +257,11 @@ The following key types are supported: - ECDSA with the P-384 (`secp384r1`) curve - ECDSA with the K-256 (`secp256k1`) curve -Each entry must have a unique (and arbitrary) `kid`, plus the key itself. -The key can either be specified inline (with the `key` property), or loaded from a file (with the `key_file` property). +Each entry must have a unique `kid`, plus the key itself. +The `kid` can be any case-sensitive string value as long as it is unique to this list; +a key’s `kid` value must be stable across restarts. +The key can either be specified inline (with the `key` property), +or loaded from a file (with the `key_file` property). The following key formats are supported: - PKCS#1 PEM or DER-encoded RSA private key @@ -740,6 +743,13 @@ upstream_oauth2: # authorization request. #forward_login_hint: false + # What to do when receiving an OIDC Backchannel logout request. + # Possible values are: + # - `do_nothing` (default): do nothing, other than validating and logging the request + # - `logout_browser_only`: Only log out the MAS 'browser session' started by this OIDC session + # - `logout_all`: Log out all sessions started by this OIDC session, including MAS 'browser sessions' and client sessions + #on_backchannel_logout: do_nothing + # How user attributes should be mapped # # Most of those attributes have two main properties: @@ -769,6 +779,12 @@ upstream_oauth2: localpart: #action: force #template: "{{ user.preferred_username }}" + + # How to handle when localpart already exists. + # Possible values are (default: fail): + # - `add` : Adds the upstream account link to the existing user, regardless of whether there is an existing link or not. + # - `fail` : Fails the upstream OAuth 2.0 login. + #on_conflict: fail # The display name is the user's display name. displayname: diff --git a/docs/setup/sso.md b/docs/setup/sso.md index 0f3994825..3442d06bd 100644 --- a/docs/setup/sso.md +++ b/docs/setup/sso.md @@ -24,6 +24,7 @@ The general configuration usually goes as follows: - `response_type`: `code` - `response_mode`: `query` - `grant_type`: `authorization_code` + - (optional) `backchannel_logout_uri`: `https:///upstream/backchannel-logout/` - fill the `upstream_oauth2` section of the configuration file with the following parameters: - `providers`: - `id`: the previously generated ULID @@ -65,6 +66,30 @@ The template has the following variables available: - `user`: an object which contains the claims from both the `id_token` and the `userinfo` endpoint - `extra_callback_parameters`: an object with the additional parameters the provider sent to the redirect URL + +## Allow linking existing user accounts + +The authentication service supports linking external provider identities to existing local user accounts. + +To enable this behavior, the following option must be explicitly set in the provider configuration: + +```yaml +claims_imports: + localpart: + on_conflict: add +``` +`on_conflict` configuration is specific to `localpart` claim_imports, it can be either: +* `add` : when a user authenticates with the provider for the first time, the system checks whether a local user already exists with a `localpart` matching the attribute mapping `localpart` , _by default `{{ user.preferred_username }}`_. If a match is found, the external identity is linked to the existing local account. +* `fail` *(default)* : fails the sso login. + +To enable this option, the `localpart` mapping must be set to either `force` or `require`. + +> ⚠️ **Security Notice** +> Enabling this option can introduce a risk of account takeover. +> +> To mitigate this risk, ensure that this option is only enabled for identity providers where you can guarantee that the attribute mapping `localpart` will reliably and uniquely correspond to the intended local user account. + + ## Multiple providers behaviour Multiple authentication methods can be configured at the same time, in which case the authentication service will let the user choose which one to use. @@ -73,6 +98,25 @@ In such cases, the `human_name` parameter of the provider configuration is used If there is only one upstream provider configured and the local password database is disabled ([`passwords.enabled`](../reference/configuration.md#passwords) is set to `false`), the authentication service will automatically trigger an authorization flow with this provider. +## Backchannel logout + +The service supports receiving [OpenID Connect Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) requests. +Those are notifications from the upstream provider that the user has logged out of the provider. + +The backchannel logout URI must be configured in the provider as `https:///upstream/backchannel-logout/`, where `` is the `id` of the provider. + +By default, the authentication service will not perform any action when receiving a backchannel logout request. +The [`on_backchannel_logout`](../reference/configuration.md#upstream_oauth2) option can be used to configure what to do when receiving a backchannel logout request. + +Possible values are: + + - `do_nothing`: Do nothing, other than validating and logging the request + - `logout_browser_only`: Only log out the MAS 'browser session' started by this OIDC session + - `logout_all`: Log out all sessions started by this OIDC session, including MAS 'browser sessions' and client sessions + +One important caveat is that `logout_all` will log out all sessions started by this upstream OIDC session, including 'remote' ones done through the Device Code flow. +Concretely, this means that if QR-code login is used to log in on a phone from a laptop, when MAS receives a backchannel logout request from the upstream provider for the laptop, MAS will also log out the session on the phone. + ## Sample configurations This section contains sample configurations for popular OIDC providers. @@ -93,12 +137,11 @@ upstream_oauth2: response_mode: "form_post" token_endpoint_auth_method: "sign_in_with_apple" sign_in_with_apple: - # Only one of the below should be filled for the private key private_key_file: "" # TO BE FILLED private_key: | # TO BE FILLED # - + team_id: "" # TO BE FILLED key_id: "" # TO BE FILLED claims_imports: @@ -386,6 +429,9 @@ Follow the [Getting Started Guide](https://www.keycloak.org/guides) to install K | Client Protocol | `openid-connect` | | Access Type | `confidential` | | Valid Redirect URIs | `https:///upstream/callback/` | + | Front channel logout | `Off` | + | Backchannel logout URL | `https:///upstream/backchannel-logout/` | + | Backchannel logout session required | `On` | 5. Click `Save` 6. On the Credentials tab, update the fields: @@ -554,4 +600,4 @@ To use a Rauthy-supported [Ephemeral Client](https://sebadob.github.io/rauthy/wo "access_token_signed_response_alg": "RS256", "id_token_signed_response_alg": "RS256" } -``` \ No newline at end of file +``` diff --git a/frontend/.editorconfig b/frontend/.editorconfig deleted file mode 100644 index 70fefa646..000000000 --- a/frontend/.editorconfig +++ /dev/null @@ -1,11 +0,0 @@ -root = true - -[*] -charset=utf-8 -end_of_line = lf - -[*.{ts,tsx,js,cjs,mjs,css,json,graphql}] -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/frontend/.gitignore b/frontend/.gitignore index 5b51a6b97..bef0bf155 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,3 +1,8 @@ +# 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. + /node_modules /dist /coverage diff --git a/frontend/.storybook/locales.ts b/frontend/.storybook/locales.ts index 090812bf0..b5f64e907 100644 --- a/frontend/.storybook/locales.ts +++ b/frontend/.storybook/locales.ts @@ -27,7 +27,7 @@ export type LocalazyMetadata = { }; const localazyMetadata: LocalazyMetadata = { - projectUrl: "https://localazy.com/p/matrix-authentication-service", + projectUrl: "https://localazy.com/p/matrix-authentication-service!v0.20", baseLocale: "en", languages: [ { @@ -172,21 +172,21 @@ const localazyMetadata: LocalazyMetadata = { file: "frontend.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", - "hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", - "nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" + "cs": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json", + "da": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json", + "de": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json", + "en": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json", + "et": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json", + "fi": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json", + "fr": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json", + "hu": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json", + "nb_NO": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json", + "nl": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json", + "pt": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json", + "ru": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json", + "sv": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json", + "uk": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json", + "zh#Hans": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json" } }, { @@ -194,21 +194,21 @@ const localazyMetadata: LocalazyMetadata = { file: "file.json", path: "", cdnFiles: { - "cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", - "da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", - "de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", - "en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", - "et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", - "fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", - "fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", - "hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", - "nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", - "nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", - "pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", - "ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", - "sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", - "uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", - "zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" + "cs": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json", + "da": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json", + "de": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json", + "en": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json", + "et": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json", + "fi": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json", + "fr": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json", + "hu": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json", + "nb_NO": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json", + "nl": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json", + "pt": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json", + "ru": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json", + "sv": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json", + "uk": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json", + "zh#Hans": "https://delivery.localazy.com/_a68674711858600910558e57bb66/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json" } } ] diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index acfd7491f..d895a0bc9 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { StorybookConfig } from "@storybook/react-vite"; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index 1f0b019a5..a9abadc7d 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { ArgTypes, diff --git a/frontend/.storybook/public/mockServiceWorker.js b/frontend/.storybook/public/mockServiceWorker.js index de7bc0f29..be4527c7e 100644 --- a/frontend/.storybook/public/mockServiceWorker.js +++ b/frontend/.storybook/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.10.2' +const PACKAGE_VERSION = '2.10.4' const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 76a256465..2020638e0 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { CodegenConfig } from "@graphql-codegen/cli"; diff --git a/frontend/i18next-parser.config.ts b/frontend/i18next-parser.config.ts index 4b003ccbd..1fa452605 100644 --- a/frontend/i18next-parser.config.ts +++ b/frontend/i18next-parser.config.ts @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { UserConfig } from "i18next-parser"; diff --git a/frontend/index.html b/frontend/index.html index 789ad52a7..e482ec272 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,9 +1,9 @@ diff --git a/frontend/knip.config.ts b/frontend/knip.config.ts index 36a69e275..b5e382961 100644 --- a/frontend/knip.config.ts +++ b/frontend/knip.config.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { KnipConfig } from "knip"; diff --git a/frontend/locales/cs.json b/frontend/locales/cs.json index e1a843475..8613ad33e 100644 --- a/frontend/locales/cs.json +++ b/frontend/locales/cs.json @@ -147,7 +147,7 @@ }, "nav": { "devices": "Zařízení", - "plan": "Plan", + "plan": "Plán", "profile": "Profil", "sessions": "Relace", "settings": "Nastavení" diff --git a/frontend/locales/da.json b/frontend/locales/da.json index 19cad7504..48a6bdff5 100644 --- a/frontend/locales/da.json +++ b/frontend/locales/da.json @@ -146,7 +146,7 @@ }, "nav": { "devices": "Enheder", - "plan": "Plan", + "plan": "Abonnementsordning", "profile": "Profil", "sessions": "Sessioner", "settings": "Indstillinger" diff --git a/frontend/locales/et.json b/frontend/locales/et.json index 5b46be41c..271e268d8 100644 --- a/frontend/locales/et.json +++ b/frontend/locales/et.json @@ -146,7 +146,7 @@ }, "nav": { "devices": "Seadmed", - "plan": "Plan", + "plan": "Teenusepakett", "profile": "Profiil", "sessions": "Sessioonid", "settings": "Seadistused" diff --git a/frontend/locales/fi.json b/frontend/locales/fi.json index 781a09334..8c11dfe85 100644 --- a/frontend/locales/fi.json +++ b/frontend/locales/fi.json @@ -146,7 +146,7 @@ }, "nav": { "devices": "Laitteet", - "plan": "Plan", + "plan": "Tilaus", "profile": "Profiili", "sessions": "Istunnot", "settings": "Asetukset" diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 225ed45e0..eb4f0b3f9 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -146,7 +146,7 @@ }, "nav": { "devices": "Appareil", - "plan": "Plan", + "plan": "Forfait", "profile": "Profil", "sessions": "Sessions", "settings": "Paramètres" diff --git a/frontend/locales/hu.json b/frontend/locales/hu.json index a6e452455..239558da3 100644 --- a/frontend/locales/hu.json +++ b/frontend/locales/hu.json @@ -146,7 +146,7 @@ }, "nav": { "devices": "Eszközök", - "plan": "Plan", + "plan": "Terv", "profile": "Profil", "sessions": "Munkamenetek", "settings": "Beállítások" diff --git a/frontend/locales/pt.json b/frontend/locales/pt.json index 51b310a16..66c4920c6 100644 --- a/frontend/locales/pt.json +++ b/frontend/locales/pt.json @@ -122,7 +122,7 @@ "web": "Web" }, "email_in_use": { - "heading": "O endereço de correio eletrónico {{email}} já está a ser utilizado." + "heading": "O endereço de e-mail {{email}} já está a ser utilizado." }, "end_session_button": { "confirmation_modal_title": "Tem a certeza de que quer terminar esta sessão?", @@ -146,7 +146,7 @@ }, "nav": { "devices": "Dispositivos", - "plan": "Plan", + "plan": "Plano", "profile": "Perfil", "sessions": "Sessões", "settings": "Configurações" @@ -172,13 +172,13 @@ "current_password_label": "Palavra-passe atual", "failure": { "description": { - "account_locked": "A sua conta está bloqueada e não pode ser recuperada neste momento. Se isso não for esperado, entre em contato com o administrador do servidor.", - "expired_recovery_ticket": "O link de recuperação expirou. Por favor, inicie o processo de recuperação de conta novamente desde o início.", + "account_locked": "A sua conta está bloqueada e não pode ser recuperada neste momento. Se isso não for esperado, entre em contacto com o administrador do servidor.", + "expired_recovery_ticket": "O link de recuperação expirou. Inicie o processo de recuperação da conta desde o início.", "invalid_new_password": "A nova senha que você escolheu é inválida; ela pode não atender à política de segurança configurada.", "no_current_password": "Você não tem uma senha atual.", - "no_such_recovery_ticket": "O link de recuperação é inválido. Se você copiou o link do e-mail de recuperação, verifique se o link completo foi copiado.", + "no_such_recovery_ticket": "O link de recuperação é inválido. Se copiou o link a partir do e-mail de recuperação, verifique se o copiou na totalidade.", "password_changes_disabled": "As alterações de palavra-passe estão desactivadas.", - "recovery_ticket_already_used": "O link de recuperação já foi usado. Não pode ser utilizado novamente.", + "recovery_ticket_already_used": "O link de recuperação já foi utilizado e não pode ser utilizado novamente.", "unspecified": "Este pode ser um problema temporário, por isso, tente novamente mais tarde. Se o problema persistir, entre em contato com o administrador do servidor.", "wrong_password": "A palavra-passe que forneceu como palavra-passe atual está incorreta. Por favor, tente novamente." }, @@ -235,10 +235,10 @@ }, "too_weak": "Esta palavra-passe é demasiado fraca", "warning": { - "common": "Esta é uma senha comumente usada.", + "common": "Esta é uma palavra-passe frequentemente utilizada.", "common_names": "Nomes e sobrenomes comuns são fáceis de adivinhar.", "dates": "As datas são fáceis de adivinhar.", - "extended_repeat": "Padrões de caracteres repetidos como \"abcabcabc\" são fáceis de adivinhar.", + "extended_repeat": "Padrões repetidos de carateres, como 'abcabcabc', são fáceis de adivinhar.", "key_pattern": "Padrões de teclado curtos são fáceis de adivinhar.", "names_by_themselves": "Nomes individuais ou sobrenomes são fáceis de adivinhar.", "pwned": "A sua palavra-passe foi exposta por uma violação de dados na Internet.", @@ -257,15 +257,15 @@ "button": "Redefinir identidade", "cancelled": { "description_1": "Pode fechar esta janela e voltar à aplicação para continuar.", - "description_2": "Se você estiver desconectado em todos os lugares e não se lembrar do código de recuperação, ainda precisará redefinir sua identidade.", + "description_2": "Caso tenha terminado sessão em todos os dispositivos e não se recorde do seu código de recuperação, continuará a ser necessário repor a sua identidade.", "heading": "Redefinição de identidade cancelada." }, "description": "Se não tiver sessão iniciada noutros dispositivos e tiver perdido a sua chave de recuperação, terá de repor a sua identidade para continuar a utilizar a aplicação.", "effect_list": { "negative_1": "Perderá o histórico de mensagens existente", - "negative_2": "Você precisará verificar todos os seus dispositivos e contatos existentes novamente", + "negative_2": "Terá de verificar novamente todos os seus dispositivos e contactos existentes.", "neutral_1": "Perderá qualquer histórico de mensagens que esteja armazenado apenas no servidor", - "neutral_2": "Você precisará verificar todos os seus dispositivos e contatos existentes novamente", + "neutral_2": "Terá de verificar novamente todos os seus dispositivos e contactos existentes.", "positive_1": "Os detalhes da sua conta, contactos, preferências e lista de conversação serão mantidos" }, "failure": { @@ -305,7 +305,7 @@ "label": "Nome do dispositivo", "title": "Editar nome do dispositivo" }, - "signed_in_date": "Conectado ", + "signed_in_date": "Sessão iniciada ", "signed_in_label": "Sessão iniciada", "title": "Detalhes do dispositivo", "unknown_browser": "Navegador desconhecido", @@ -357,9 +357,9 @@ "user_sessions_overview": { "active_sessions:one": "{{count}} sessão ativa", "active_sessions:other": "{{count}} sessões ativas", - "heading": "Onde você está conectado", + "heading": "Onde tem sessão iniciada", "no_active_sessions": { - "default": "Você não está conectado a nenhum aplicativo.", + "default": "Não tem sessão iniciada em nenhuma aplicação.", "inactive_90_days": "Todas as suas sessões estiveram ativas nos últimos 90 dias." } }, diff --git a/frontend/locales/uk.json b/frontend/locales/uk.json index cb5434697..af655db0f 100644 --- a/frontend/locales/uk.json +++ b/frontend/locales/uk.json @@ -147,7 +147,7 @@ }, "nav": { "devices": "Пристрої", - "plan": "Plan", + "plan": "План", "profile": "Профіль", "sessions": "Сеанси", "settings": "Налаштування" diff --git a/frontend/locales/zh-Hans.json b/frontend/locales/zh-Hans.json index e54bd1f99..cbb1cb7b5 100644 --- a/frontend/locales/zh-Hans.json +++ b/frontend/locales/zh-Hans.json @@ -4,11 +4,11 @@ "cancel": "取消", "clear": "清除", "close": "关闭", - "collapse": "Collapse", - "confirm": "Confirm", + "collapse": "折叠", + "confirm": "确认", "continue": "继续", "edit": "编辑", - "expand": "Expand", + "expand": "展开", "save": "保存", "save_and_continue": "保存并继续", "sign_out": "注销", @@ -26,30 +26,30 @@ }, "common": { "add": "添加", - "e2ee": "End-to-end encryption", + "e2ee": "端到端加密", "error": "错误", "loading": "加载中...", - "next": "下一步", - "password": "Password", - "previous": "上一步", + "next": "下一页", + "password": "密码", + "previous": "上一页", "saved": "已保存", "saving": "正在保存..." }, "frontend": { "account": { - "account_password": "Account password", - "contact_info": "Contact info", + "account_password": "账户密码", + "contact_info": "联系方式", "delete_account": { - "alert_description": "This account will be permanently erased and you’ll no longer have access to any of your messages.", - "alert_title": "You’re about to lose all of your data", - "button": "Delete account", - "dialog_description": "Confirm that you would like to delete your account:\n\n\nYou will not be able to reactivate your account\nYou will no longer be able to sign in\nNo one will be able to reuse your username (MXID), including you\nYou will leave all rooms and direct messages you are in\nYou will be removed from the identity server, and no one will be able to find you with your email or phone number\n\nYour old messages will still be visible to people who received them. Would you like to hide your send messages from people who join rooms in the future?", - "dialog_title": "Delete this account?", - "erase_checkbox_label": "Yes, hide all my messages from new joiners", - "incorrect_password": "Incorrect password, please try again", - "mxid_label": "Confirm your Matrix ID ({{ mxid }})", - "mxid_mismatch": "This value does not match your Matrix ID", - "password_label": "Enter your password to continue" + "alert_description": "此账户将被永久删除,你将无法再访问任何消息。", + "alert_title": "你将丢失所有数据", + "button": "删除账户", + "dialog_description": "确认你想删除账户:\n\n\n你将无法重新激活账户\n你将无法再登录\n包括你在内,没有人能重复使用此用户名(MXID)\n你将离开所有房间与私聊\n你将被从身份服务器中移除,遂没有人能通过你的邮件地址或电话号码找到你\n\n收到过你曾经的消息的人员仍然能看到你的历史消息。是否向未来加入房间的人员隐藏你发送的消息?", + "dialog_title": "删除此账户?", + "erase_checkbox_label": "是,对新加入者隐藏我的所有消息", + "incorrect_password": "密码不正确,请重试。", + "mxid_label": "确认你的 Matrix ID({{ mxid }})", + "mxid_mismatch": "此值与你的 Matrix ID 不匹配", + "password_label": "输入密码以继续" }, "edit_profile": { "display_name_help": "无论您在哪里登录,其他人都会看到此内容。", @@ -63,8 +63,8 @@ "label": "密码" }, "sign_out": { - "button": "Sign out of account", - "dialog": "Sign out of this account?" + "button": "注销登录", + "dialog": "注销此账户?" }, "title": "你的账户" }, @@ -81,14 +81,14 @@ "email_exists_error": "输入的电子邮件地址已添加到此账户", "email_field_help": "添加可用于访问此账户的备用电子邮件地址。", "email_field_label": "添加电子邮件地址", - "email_in_use_error": "The entered email is already in use", + "email_in_use_error": "输入的邮件地址已被使用", "email_invalid_alert": { "text": "输入的电子邮件地址无效", "title": "无效的电子邮件地址" }, "email_invalid_error": "输入的电子邮件地址无效", - "incorrect_password_error": "Incorrect password, please try again", - "password_confirmation": "Confirm your account password to add this email address" + "incorrect_password_error": "密码不正确,请重试", + "password_confirmation": "确认账户密码以添加此邮件地址" }, "app_sessions_list": { "error": "加载应用程序会话失败", @@ -121,11 +121,11 @@ "web": "网页" }, "email_in_use": { - "heading": "The email address {{email}} is already in use." + "heading": "此邮件地址 {{email}} 已被使用。" }, "end_session_button": { "confirmation_modal_title": "你确定要结束这个会话吗?", - "text": "登出" + "text": "移除设备" }, "error": { "hideDetails": "隐藏详细信息", @@ -196,12 +196,12 @@ }, "password_reset": { "consumed": { - "subtitle": "To create a new password, start over and select ”Forgot password“.", + "subtitle": "要创建新密码,请重新开始并选择“忘记密码”。", "title": "重置密码的链接已被使用" }, "expired": { "resend_email": "重新发送电子邮件", - "subtitle": "Request a new email that will be sent to: {{email}}", + "subtitle": "请求发送新邮件到:{{email}}", "title": "重置密码的链接已过期" }, "subtitle": "为您的账户选择一个新密码。", @@ -253,34 +253,34 @@ } }, "reset_cross_signing": { - "button": "允许重置加密身份", + "button": "重置身份", "cancelled": { - "description_1": "You can close this window and go back to the app to continue.", - "description_2": "If you're signed out everywhere and don't remember your recovery code, you'll still need to reset your identity.", - "heading": "Identity reset cancelled." + "description_1": "你可以关闭此窗口并返回到 App 以继续。", + "description_2": "若你在任何地方都已注销并且忘记恢复代码,你仍然需要重置身份。", + "heading": "身份重置流程已被取消。" }, "description": "如果您没有在其他地方登录,并且忘记或丢失了所有恢复选项,则需要重置您的加密身份。这意味着您将丢失现有的信息历史记录,其他用户会看到您已重置身份,您需要再次验证现有设备。", "effect_list": { - "negative_1": "You will lose your existing message history", - "negative_2": "You will need to verify all your existing devices and contacts again", - "neutral_1": "You will lose any message history that's stored only on the server", - "neutral_2": "You will need to verify all your existing devices and contacts again", - "positive_1": "Your account details, contacts, preferences, and chat list will be kept" + "negative_1": "你将丢失现有消息历史", + "negative_2": "你将需要再次验证所有现有设备与联系人", + "neutral_1": "你将丢失仅存储在服务器上的消息历史", + "neutral_2": "你将需要再次验证所有现有设备与联系人", + "positive_1": "你的账户的详细信息、联系人、偏好与聊天列表都将被保留" }, "failure": { "description": "这可能是暂时的问题,请稍后再试。如果问题仍然存在,请联系服务器管理员。", - "heading": "Failed to allow crypto identity reset", + "heading": "加密身份重置授权失败。", "title": "无法允许加密身份" }, - "finish_reset": "Finish reset", + "finish_reset": "完成重置", "heading": "重置加密身份", - "start_reset": "Start reset", + "start_reset": "开始重置", "success": { - "description": "客户端现在可以临时重置您的账户加密身份。请按照客户端中的说明完成该过程。", - "heading": "Identity reset successfully. Go back to the app to finish the process.", + "description": "身份重置已获批准,在接下来的 {{minutes}} 分钟内有效。您可以关闭此窗口并返回应用继续操作。", + "heading": "已成功重置身份。返回到 App 以完成此流程。", "title": "临时允许重置加密身份" }, - "warning": "Only reset your identity if you don't have access to another signed-in device and you've lost your recovery key." + "warning": "仅当你无法访问其它已登录的设备并且丢失了恢复密钥时才重置身份。" }, "selectable_session": { "label": "选择会话" @@ -292,7 +292,7 @@ "device_id_label": "设备 ID", "finished_date": "已完成 ", "finished_label": "已完成", - "generic_browser_session": "Browser session", + "generic_browser_session": "浏览器会话", "id_label": "ID", "ip_label": "IP 地址", "last_active_label": "最后活动", @@ -300,9 +300,9 @@ "name_for_platform": "{{name}}对于 {{platform}}", "scopes_label": "范围", "set_device_name": { - "help": "Set a name that will help you identify this device.", - "label": "Device name", - "title": "Edit device name" + "help": "设置一个名称有助于识别此设备。", + "label": "设备名称", + "title": "编辑设备名称" }, "signed_in_date": "已登录", "signed_in_label": "已登录", @@ -331,8 +331,8 @@ "delete_button_confirmation_modal": { "action": "删除电子邮件地址", "body": "您确定要删除此电子邮件地址吗?", - "incorrect_password": "Incorrect password, please try again", - "password_confirmation": "Confirm your account password to delete this email address" + "incorrect_password": "密码不正确,请重试", + "password_confirmation": "确认账户密码以删除此邮件地址" }, "delete_button_title": "删除电子邮件地址", "email": "电子邮件地址", @@ -362,8 +362,8 @@ }, "verify_email": { "code_expired_alert": { - "description": "The code has expired. Please request a new code.", - "title": "Code expired" + "description": "此代码已过期,请重新请求新代码。", + "title": "代码已过期" }, "code_field_error": "无法识别代码", "code_field_label": "6位数代码", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 47a9466b6..56f9f1388 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,59 +12,59 @@ "@fontsource/inter": "^5.2.6", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", - "@tanstack/react-query": "^5.80.7", - "@tanstack/react-router": "^1.121.2", - "@vector-im/compound-design-tokens": "4.0.4", - "@vector-im/compound-web": "^8.0.0", + "@tanstack/react-query": "^5.84.1", + "@tanstack/react-router": "^1.130.10", + "@vector-im/compound-design-tokens": "5.0.2", + "@vector-im/compound-web": "^8.2.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "classnames": "^2.5.1", "date-fns": "^4.1.0", - "i18next": "^25.2.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-i18next": "^15.5.2", - "swagger-ui-dist": "^5.24.1", + "i18next": "^25.3.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-i18next": "^15.6.1", + "swagger-ui-dist": "^5.27.0", "valibot": "^1.1.0", "vaul": "^1.1.2" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "^2.1.2", "@browser-logos/chrome": "^2.0.0", "@browser-logos/firefox": "^3.0.10", "@browser-logos/safari": "^2.1.0", "@codecov/vite-plugin": "^1.9.1", "@graphql-codegen/cli": "^5.0.7", - "@graphql-codegen/client-preset": "^4.8.2", + "@graphql-codegen/client-preset": "^4.8.3", "@graphql-codegen/typescript-msw": "^3.0.1", - "@storybook/addon-docs": "^9.0.8", - "@storybook/react-vite": "^9.0.8", - "@tanstack/react-query-devtools": "^5.80.7", - "@tanstack/react-router-devtools": "^1.121.5", - "@tanstack/router-plugin": "^1.121.4", - "@testing-library/jest-dom": "^6.6.3", + "@storybook/addon-docs": "^9.1.1", + "@storybook/react-vite": "^9.1.1", + "@tanstack/react-query-devtools": "^5.84.1", + "@tanstack/react-router-devtools": "^1.130.10", + "@tanstack/router-plugin": "^1.130.10", + "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.0.1", - "@types/react": "19.1.8", - "@types/react-dom": "19.1.6", - "@types/swagger-ui-dist": "^3.30.5", + "@types/node": "^24.1.0", + "@types/react": "19.1.9", + "@types/react-dom": "19.1.7", + "@types/swagger-ui-dist": "^3.30.6", "@vitejs/plugin-react": "^4.5.2", - "@vitest/coverage-v8": "^3.2.3", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.21", "browserslist-to-esbuild": "^2.1.1", "graphql": "^16.11.0", "happy-dom": "^18.0.1", "i18next-parser": "^9.3.0", - "knip": "^5.61.0", - "msw": "^2.10.2", + "knip": "^5.62.0", + "msw": "^2.10.4", "msw-storybook-addon": "^2.0.5", - "postcss": "^8.5.5", - "postcss-import": "^16.1.0", + "postcss": "^8.5.6", + "postcss-import": "^16.1.1", "postcss-nesting": "^13.0.2", "rimraf": "^6.0.1", "storybook": "^9.0.1", - "storybook-react-i18next": "4.0.7", + "storybook-react-i18next": "4.0.11", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "vite": "6.3.5", @@ -214,22 +214,21 @@ } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -255,16 +254,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -343,6 +341,15 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -489,13 +496,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1003,13 +1009,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", - "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", @@ -1067,30 +1073,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -1110,11 +1114,10 @@ } }, "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz", + "integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==", "dev": true, - "hasInstallScript": true, "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" @@ -1127,20 +1130,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" + "@biomejs/cli-darwin-arm64": "2.1.2", + "@biomejs/cli-darwin-x64": "2.1.2", + "@biomejs/cli-linux-arm64": "2.1.2", + "@biomejs/cli-linux-arm64-musl": "2.1.2", + "@biomejs/cli-linux-x64": "2.1.2", + "@biomejs/cli-linux-x64-musl": "2.1.2", + "@biomejs/cli-win32-arm64": "2.1.2", + "@biomejs/cli-win32-x64": "2.1.2" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.2.tgz", + "integrity": "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==", "cpu": [ "arm64" ], @@ -1155,9 +1158,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.2.tgz", + "integrity": "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==", "cpu": [ "x64" ], @@ -1172,9 +1175,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.2.tgz", + "integrity": "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==", "cpu": [ "arm64" ], @@ -1189,9 +1192,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.2.tgz", + "integrity": "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==", "cpu": [ "arm64" ], @@ -1206,9 +1209,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.2.tgz", + "integrity": "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==", "cpu": [ "x64" ], @@ -1223,9 +1226,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.2.tgz", + "integrity": "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==", "cpu": [ "x64" ], @@ -1240,9 +1243,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.2.tgz", + "integrity": "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==", "cpu": [ "arm64" ], @@ -1257,9 +1260,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.2.tgz", + "integrity": "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==", "cpu": [ "x64" ], @@ -2068,18 +2071,17 @@ } }, "node_modules/@graphql-codegen/client-preset": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.8.2.tgz", - "integrity": "sha512-YoH2obkNLorgT7bs5cbveg6A1fM4ZW5AE/CWLaSzViMTAXk51q0z/5+sTrDW2Ft6Or3mTxFLEByCgXhPgAj2Lw==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-4.8.3.tgz", + "integrity": "sha512-QpEsPSO9fnRxA6Z66AmBuGcwHjZ6dYSxYo5ycMlYgSPzAbyG8gn/kWljofjJfWqSY+T/lRn+r8IXTH14ml24vQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/template": "^7.20.7", "@graphql-codegen/add": "^5.0.3", "@graphql-codegen/gql-tag-operations": "4.0.17", "@graphql-codegen/plugin-helpers": "^5.1.1", - "@graphql-codegen/typed-document-node": "^5.1.1", + "@graphql-codegen/typed-document-node": "^5.1.2", "@graphql-codegen/typescript": "^4.1.6", "@graphql-codegen/typescript-operations": "^4.6.1", "@graphql-codegen/visitor-plugin-common": "^5.8.0", @@ -2209,11 +2211,10 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typed-document-node": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.1.1.tgz", - "integrity": "sha512-Bp/BrMZDKRwzuVeLv+pSljneqONM7gqu57ZaV34Jbncu2hZWMRDMfizTKghoEwwZbRCYYfJO9tA0sYVVIfI1kg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-5.1.2.tgz", + "integrity": "sha512-jaxfViDqFRbNQmfKwUY8hDyjnLTw2Z7DhGutxoOiiAI0gE/LfPe0LYaVFKVmVOOD7M3bWxoWfu4slrkbWbUbEw==", "dev": true, - "license": "MIT", "dependencies": { "@graphql-codegen/plugin-helpers": "^5.1.0", "@graphql-codegen/visitor-plugin-common": "5.8.0", @@ -2232,8 +2233,7 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" + "dev": true }, "node_modules/@graphql-codegen/typescript": { "version": "4.1.6", @@ -3390,11 +3390,10 @@ } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.0.tgz", - "integrity": "sha512-dPo6SE4dm8UKcgGg4LsV9iw6f5HkIeJwzMA2M2Lb+mhl5vxesbDvb3ENTzNTkGnOxS6PqJig2pfXdtYaW3S9fg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", + "integrity": "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==", "dev": true, - "license": "MIT", "dependencies": { "glob": "^10.0.0", "magic-string": "^0.30.0", @@ -3402,7 +3401,7 @@ }, "peerDependencies": { "typescript": ">= 4.3.x", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3411,18 +3410,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3435,16 +3429,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -3453,11 +3437,10 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -4993,16 +4976,16 @@ "license": "Apache-2.0" }, "node_modules/@storybook/addon-docs": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.0.8.tgz", - "integrity": "sha512-YRR8qHitwXVTJyn02YMrzd9mCKcuZWSKWt+J/ddFb8khGfLcAW+O0NohGeqMyM6XStLVDKKIKsMoTHggUwIFsA==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.1.tgz", + "integrity": "sha512-CzgvTy3V5X4fe+VPkiZVwPKARlpEBDAKte8ajLAlHJQLFpADdYrBRQ0se6I+kcxva7rZQzdhuH7qjXMDRVcfnw==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "9.0.8", - "@storybook/icons": "^1.2.12", - "@storybook/react-dom-shim": "9.0.8", + "@storybook/csf-plugin": "9.1.1", + "@storybook/icons": "^1.4.0", + "@storybook/react-dom-shim": "9.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -5012,17 +4995,17 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.0.8" + "storybook": "^9.1.1" } }, "node_modules/@storybook/builder-vite": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.0.8.tgz", - "integrity": "sha512-dbwDfBUwLW8I71h0Y9r+twtEdjKC+oBP1YweS26ET78qc6qXMFsK5Tfh6lzj/vQbmxY0YhnTVrrkXgfR0erLPA==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.1.tgz", + "integrity": "sha512-rM0QOfykr39SFBRQnoAa5PU3xTHnJE1R5tigvjved1o7sumcfjrhqmEyAgNZv1SoRztOO92jwkTi7En6yheOKg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "9.0.8", + "@storybook/csf-plugin": "9.1.1", "ts-dedent": "^2.0.0" }, "funding": { @@ -5030,14 +5013,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.0.8", - "vite": "^5.0.0 || ^6.0.0" + "storybook": "^9.1.1", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.0.8.tgz", - "integrity": "sha512-mNjo4t9liAbQvhE9ni87NU2sz9tqFU4Y54ioSFDlW24wpubsvnhBi5h4z3EkeQJSzIzNMRym9SC7Elbqa3Kf+g==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.1.tgz", + "integrity": "sha512-MwdtvzzFpkard06pCfDrgRXZiBfWAQICdKh7kzpv1L8SwewsRgUr5WZQuEAVfYdSvCFJbWnNN4KirzPhe5ENCg==", "dev": true, "license": "MIT", "dependencies": { @@ -5048,7 +5031,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.0.8" + "storybook": "^9.1.1" } }, "node_modules/@storybook/global": { @@ -5073,14 +5056,14 @@ } }, "node_modules/@storybook/react": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.0.8.tgz", - "integrity": "sha512-in3O+lDmxKRhdcX3Wg6FbLnb2/PuqRL+rUKMz1wr1ndSkw4J1jGsvP909oEEYnDbjHOX0xnNxxbEapO4F9fgBQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.1.tgz", + "integrity": "sha512-F5vRFxDf1fzM6CG88olrzEH03iP6C1YAr4/nr5bkLNs6TNm9Hh7KmRVG2jFtoy5w9uCwbQ9RdY+TrRbBI7n67g==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "9.0.8" + "@storybook/react-dom-shim": "9.1.1" }, "engines": { "node": ">=20.0.0" @@ -5092,7 +5075,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.0.8", + "storybook": "^9.1.1", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -5102,9 +5085,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.0.8.tgz", - "integrity": "sha512-SYyjRagHZx724hGEWSZcXRzj82am77OpqeA9ps6ZsCSn4cVY9FORGEeY2bnlQkpLnDUH5yjdV/oh+0fXDbl/8g==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.1.tgz", + "integrity": "sha512-L+HCOXvOP+PwKrVS8od9aF+F4hO7zA0Nt1vnpbg2LeAHCxYghrjFVtioe7gSlzrlYdozQrPLY98a4OkDB7KGrw==", "dev": true, "license": "MIT", "funding": { @@ -5114,21 +5097,21 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.0.8" + "storybook": "^9.1.1" } }, "node_modules/@storybook/react-vite": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.0.8.tgz", - "integrity": "sha512-nAtT9UeOkKCBJ2kbatC7Hf/TX3Kl+e21wvc1D5xpS86ulPamzjzxLD5nW+vliBKePOo+9ZW/KQZYCLg3snRJtQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.1.tgz", + "integrity": "sha512-9rMjAqgrcuVF/GS171fYSLuUs5QC3e0WPpIm2JOP7Z9qWctM1ApVb9UCYY7ZNl9Gc3kvjKsK5J1+A4Zw4a2+ag==", "dev": true, "license": "MIT", "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.0", + "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "9.0.8", - "@storybook/react": "9.0.8", - "find-up": "^5.0.0", + "@storybook/builder-vite": "9.1.1", + "@storybook/react": "9.1.1", + "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", @@ -5144,14 +5127,14 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.0.8", - "vite": "^5.0.0 || ^6.0.0" + "storybook": "^9.1.1", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@tanstack/history": { - "version": "1.120.17", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.120.17.tgz", - "integrity": "sha512-k07LFI4Qo074IIaWzT/XjD0KlkGx2w1V3fnNtclKx0oAl8z4O9kCh6za+FPEIRe98xLgNFEiddDbJeAYGSlPtw==", + "version": "1.129.7", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.129.7.tgz", + "integrity": "sha512-I3YTkbe4RZQN54Qw4+IUhOjqG2DdbG2+EBWuQfew4MEk0eddLYAQVa50BZVww4/D2eh5I9vEk2Fd1Y0Wty7pug==", "license": "MIT", "engines": { "node": ">=12" @@ -5162,9 +5145,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz", - "integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==", + "version": "5.83.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz", + "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==", "license": "MIT", "funding": { "type": "github", @@ -5172,9 +5155,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.80.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.80.0.tgz", - "integrity": "sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA==", + "version": "5.84.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", + "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==", "dev": true, "license": "MIT", "funding": { @@ -5183,12 +5166,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz", - "integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==", + "version": "5.84.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz", + "integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.80.7" + "@tanstack/query-core": "5.83.1" }, "funding": { "type": "github", @@ -5199,33 +5182,33 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.80.7.tgz", - "integrity": "sha512-7Dz/19fVo0i+jgLVBabV5vfGOlLyN5L1w8w1/ogFhe6ItNNsNA+ZgNTbtiKpbR3CcX2WDRRTInz1uMSmHzTsoQ==", + "version": "5.84.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.84.1.tgz", + "integrity": "sha512-nle+OQ9B3Z3EG2R3ixvaNcJ6OeqGwmAc5iMDW6Vj+emLZkWRrN3BDsrzZQu414n34lpxplnC7z1jmKuU/scHCQ==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.80.0" + "@tanstack/query-devtools": "5.84.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.80.7", + "@tanstack/react-query": "^5.84.1", "react": "^18 || ^19" } }, "node_modules/@tanstack/react-router": { - "version": "1.121.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.121.2.tgz", - "integrity": "sha512-+VqITx7ZqQjB/H0ujlgs+uzaIElff1X66uh9Tgi9Bx3DTrPSzdNAKs3Osk8UQ7rL1hbHiW3ZT1vQPJU08cfKrQ==", + "version": "1.130.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.10.tgz", + "integrity": "sha512-AxhKYEBfdL1nQCh6y/dRVy6oHDLDE7Mu9t7Pl1l6C7Z5dJQ6e5Ld3+7QK6HFGwXBj8WBAdGa0ulj916JwejSEA==", "license": "MIT", "dependencies": { - "@tanstack/history": "1.120.17", + "@tanstack/history": "1.129.7", "@tanstack/react-store": "^0.7.0", - "@tanstack/router-core": "1.121.2", - "jsesc": "^3.1.0", + "@tanstack/router-core": "1.130.10", + "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, @@ -5242,13 +5225,13 @@ } }, "node_modules/@tanstack/react-router-devtools": { - "version": "1.121.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.121.5.tgz", - "integrity": "sha512-HQ/zQhywPHpsjpGZnmgUkEvxll3cUZItFUVPqOHXuorgEBfzjK8/jTH2WZpLHt6HyxJq3M8yRUvStoDd2VIxCA==", + "version": "1.130.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.130.10.tgz", + "integrity": "sha512-D9/OFOgDiBHEHCNM6q/bcCyBDXint9iR+DL25h6ACoJOIztNyGtVhvzrJtrhlImG5HzSo8gARXIFCTVeCBP0xw==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/router-devtools-core": "^1.121.2" + "@tanstack/router-devtools-core": "1.130.10" }, "engines": { "node": ">=12" @@ -5258,11 +5241,41 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-router": "^1.121.2", + "@tanstack/react-router": "^1.130.10", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, + "node_modules/@tanstack/react-router-devtools/node_modules/@tanstack/router-devtools-core": { + "version": "1.130.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.130.10.tgz", + "integrity": "sha512-DkI1LVsmzX99GESkeH0hEe75+kO5zobLgJ8xFro7Y0iI1DANImpHkaqesSofaovB02uN6BPLxdMjKJ2EmFzknw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16", + "solid-js": "^1.9.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.130.10", + "csstype": "^3.0.10", + "solid-js": ">=1.9.5", + "tiny-invariant": "^1.3.3" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, "node_modules/@tanstack/react-store": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.1.tgz", @@ -5282,14 +5295,18 @@ } }, "node_modules/@tanstack/router-core": { - "version": "1.121.2", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.121.2.tgz", - "integrity": "sha512-jwkhTTDUuhwWq6CeOwvJFcqXagmFu+2FSCx1WHj+mt516hDe7+qe5JWQ7KqMaAbV92Jue/qNnrRxMEONz8aKBg==", + "version": "1.130.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.10.tgz", + "integrity": "sha512-Fj+sGGz2maEBybLSIebocJHxMSd7IhLjb2JBtEyhm306e39Yj4pYQHmnTQA1m494xAAWYw1mxpHt9uOYkot3Eg==", "license": "MIT", "dependencies": { - "@tanstack/history": "1.120.17", + "@tanstack/history": "1.129.7", "@tanstack/store": "^0.7.0", - "tiny-invariant": "^1.3.3" + "cookie-es": "^1.2.2", + "seroval": "^1.3.2", + "seroval-plugins": "^1.3.2", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" }, "engines": { "node": ">=12" @@ -5299,45 +5316,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tanstack/router-devtools-core": { - "version": "1.121.2", - "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.121.2.tgz", - "integrity": "sha512-LRuz0MyEw7swouwtbYT3MfaRVXOtUNAE8Vxuy9ETH6dnvXX+wm8kHqcHZX3AlDrL6fHi86aDKEXFJg+MyWZzcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "goober": "^2.1.16" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/router-core": "^1.121.2", - "csstype": "^3.0.10", - "solid-js": ">=1.9.5", - "tiny-invariant": "^1.3.3" - }, - "peerDependenciesMeta": { - "csstype": { - "optional": true - } - } - }, "node_modules/@tanstack/router-generator": { - "version": "1.121.2", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.121.2.tgz", - "integrity": "sha512-3KIQwY6INW2y2SmBWs+xoGyt6GM8Q96QDJW9sbtLdiTbddvVGyphVAs3Vai2HoTNsUdkw0Fnd2QfgPUwY4PSQQ==", + "version": "1.130.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.130.10.tgz", + "integrity": "sha512-GN3yAtvN0PgYQe3nf9NtB1+zJRaODtRbENCVW7PbyH3cJYh612MbhshekYMxxeyFDro9+FQ/73qvSqygTFQVkw==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/router-core": "^1.121.2", - "@tanstack/router-utils": "^1.121.0", - "@tanstack/virtual-file-routes": "^1.120.17", + "@tanstack/router-core": "1.130.10", + "@tanstack/router-utils": "1.129.7", + "@tanstack/virtual-file-routes": "1.129.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", @@ -5353,22 +5341,22 @@ } }, "node_modules/@tanstack/router-plugin": { - "version": "1.121.4", - "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.121.4.tgz", - "integrity": "sha512-HWqpfX1rqs+A1Q2ucN64X3JAb34MdVknfEbY5W0L5+LyPHlCw9HJwPXY+i8gXD+YFTizTysCY/Go26OxpebMZA==", + "version": "1.130.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.130.10.tgz", + "integrity": "sha512-hFgT3nhPbn6NQnUFC+Zjm6tMNU30kLnpFjTB3Ienk1WSiAWkR+PJ1hnQJwSIr/A2C1UC57sLbSf/10H8YRIrNA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.8", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9", - "@babel/template": "^7.26.8", - "@babel/traverse": "^7.26.8", - "@babel/types": "^7.26.8", - "@tanstack/router-core": "^1.121.2", - "@tanstack/router-generator": "^1.121.2", - "@tanstack/router-utils": "^1.121.0", - "@tanstack/virtual-file-routes": "^1.120.17", + "@babel/core": "^7.27.7", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "@tanstack/router-core": "1.130.10", + "@tanstack/router-generator": "1.130.10", + "@tanstack/router-utils": "1.129.7", + "@tanstack/virtual-file-routes": "1.129.7", "babel-dead-code-elimination": "^1.0.10", "chokidar": "^3.6.0", "unplugin": "^2.1.2", @@ -5383,7 +5371,7 @@ }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", - "@tanstack/react-router": "^1.121.2", + "@tanstack/react-router": "^1.130.10", "vite": ">=5.0.0 || >=6.0.0", "vite-plugin-solid": "^2.11.2", "webpack": ">=5.92.0" @@ -5422,9 +5410,9 @@ } }, "node_modules/@tanstack/router-utils": { - "version": "1.121.0", - "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.121.0.tgz", - "integrity": "sha512-+gOHZdEVjOTTdk8Z7J/NVG0KdvzxFeUYjINYZEqQDRKoxEg8f+Npram0MXGy8N15OyZrsm+KHR1vMFZ2yEvZkw==", + "version": "1.129.7", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.129.7.tgz", + "integrity": "sha512-I2OyQF5U6sxHJApXKCUmCncTHKcpj4681FwyxpYg5QYOatHcn/zVMl7Rj4h36fu8/Lo2ZRLxUMd5kmXgp5Pb/A==", "dev": true, "license": "MIT", "dependencies": { @@ -5454,9 +5442,9 @@ } }, "node_modules/@tanstack/virtual-file-routes": { - "version": "1.120.17", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.120.17.tgz", - "integrity": "sha512-Ssi+yKcjG9ru02ieCpUBF7QQBEKGB7WQS1R9va3GHu+Oq9WjzmJ4rifzdugjTeKD3yfT7d1I+pOxRhoWog6CHw==", + "version": "1.129.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.129.7.tgz", + "integrity": "sha512-a+MxoAXG+Sq94Jp67OtveKOp2vQq75AWdVI8DRt6w19B0NEqpfm784FTLbVp/qdR1wmxCOmKAvElGSIiBOx5OQ==", "dev": true, "license": "MIT", "engines": { @@ -5489,18 +5477,18 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", - "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz", + "integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==", "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", - "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", + "picocolors": "^1.1.1", "redent": "^3.0.0" }, "engines": { @@ -5509,20 +5497,6 @@ "yarn": ">=1" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", @@ -5696,9 +5670,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", - "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "dev": true, "license": "MIT", "dependencies": { @@ -5706,9 +5680,9 @@ } }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5716,9 +5690,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -5740,9 +5714,9 @@ "license": "MIT" }, "node_modules/@types/swagger-ui-dist": { - "version": "3.30.5", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-dist/-/swagger-ui-dist-3.30.5.tgz", - "integrity": "sha512-SrXhD9L8qeIxJzN+o1kmf3wXeVf/+Km3jIdRM1+Yq3I5b/dlF5TcGr5WCVM7I/cBYpgf43/gCPIucQ13AhICiw==", + "version": "3.30.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-dist/-/swagger-ui-dist-3.30.6.tgz", + "integrity": "sha512-FVxN7wjLYRtJsZBscOcOcf8oR++m38vbUFjT33Mr9HBuasX9bRDrJsp7iwixcOtKSHEEa2B7o2+4wEiXqC+Ebw==", "dev": true, "license": "MIT" }, @@ -5778,9 +5752,9 @@ } }, "node_modules/@vector-im/compound-design-tokens": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.4.tgz", - "integrity": "sha512-nn62mF6Js/h75zpHcmDccrT3qtCwxcDm6gWqBZpOoSD0lBcMCinBnpAZnFVpDw2geBBpQ4xtBvnQCJ7KQE7UtQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vector-im/compound-design-tokens/-/compound-design-tokens-5.0.2.tgz", + "integrity": "sha512-LcdrGY9qktuSs9TNX+DdGGq64vP7Qk5FiiqtZBr4PEk+hCQPEyRtKDfkXbAST+0tpAjUqVp5pzlOqNUKhpIhfg==", "license": "SEE LICENSE IN README.md", "peerDependencies": { "@types/react": "*", @@ -5796,9 +5770,9 @@ } }, "node_modules/@vector-im/compound-web": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@vector-im/compound-web/-/compound-web-8.0.0.tgz", - "integrity": "sha512-VAwCGl0KMjN+qEKflnQOB1LidSsxSDiczEWka1IGKj52EzrNOfY0wlfCs73v+H84zUanYKlgOwnQgWU5at9Q/w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@vector-im/compound-web/-/compound-web-8.2.0.tgz", + "integrity": "sha512-we+EQ/pw2YCEl7EMPdpeqP3HZpnQcCuOHoiAYKFwF4doXBDENLpTyA8ZdX0cViT3sqvExPT0RHZ2Nlt5Y6dQNQ==", "license": "SEE LICENSE IN README.md", "dependencies": { "@floating-ui/react": "^0.27.0", @@ -5815,7 +5789,7 @@ "@fontsource/inconsolata": "^5", "@fontsource/inter": "^5", "@types/react": "*", - "@vector-im/compound-design-tokens": ">=1.6.1 <5.0.0", + "@vector-im/compound-design-tokens": ">=1.6.1 <6.0.0", "react": "^18 || ^19.0.0" }, "peerDependenciesMeta": { @@ -5846,9 +5820,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.3.tgz", - "integrity": "sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5870,8 +5844,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.3", - "vitest": "3.2.3" + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -5880,14 +5854,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.9", - "@vitest/utils": "3.0.9", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -5896,13 +5870,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", - "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.3", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -5922,19 +5896,6 @@ } } }, - "node_modules/@vitest/mocker/node_modules/@vitest/spy": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", - "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker/node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -5945,22 +5906,11 @@ "@types/estree": "^1.0.0" } }, - "node_modules/@vitest/mocker/node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/pretty-format": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, - "license": "MIT", "dependencies": { "tinyrainbow": "^2.0.0" }, @@ -5969,13 +5919,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", - "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.3", + "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -5983,42 +5933,14 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", - "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", - "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.3", - "loupe": "^3.1.3", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/snapshot": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", - "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.3", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6026,41 +5948,26 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", - "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, - "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, - "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.9", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -7320,6 +7227,11 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -8125,22 +8037,33 @@ } }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -8804,9 +8727,9 @@ } }, "node_modules/i18next": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz", - "integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==", + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.2.tgz", + "integrity": "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==", "funding": [ { "type": "individual", @@ -8823,7 +8746,7 @@ ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.1" + "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" @@ -9341,6 +9264,14 @@ "dev": true, "license": "MIT" }, + "node_modules/isbot": { + "version": "5.1.28", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.28.tgz", + "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==", + "engines": { + "node": ">=18" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9472,6 +9403,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -9528,9 +9460,9 @@ } }, "node_modules/knip": { - "version": "5.61.0", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.61.0.tgz", - "integrity": "sha512-g4zwSPUoSCWXWTHshNgtsiuM90jnLpZyBZ6J7BQaydfoOjclgh4NfrD7BL+TjChgDbeYg5bEQcERKK/vdJlpmA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.62.0.tgz", + "integrity": "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==", "dev": true, "funding": [ { @@ -9661,16 +9593,16 @@ } }, "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "p-locate": "^6.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9758,9 +9690,9 @@ } }, "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, @@ -10033,9 +9965,9 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.10.2.tgz", - "integrity": "sha512-RCKM6IZseZQCWcSWlutdf590M8nVfRHG1ImwzOtwz8IYxgT4zhUO0rfTcTvDGiaFE0Rhcc+h43lcF3Jc9gFtwQ==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.10.4.tgz", + "integrity": "sha512-6R1or/qyele7q3RyPwNuvc0IxO8L8/Aim6Sz5ncXEgcWUNxSKE+udriTOWHtpMwmfkLYlacA2y7TIx4cL5lgHA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10408,16 +10340,45 @@ } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10755,9 +10716,9 @@ } }, "node_modules/postcss": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", - "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -10784,9 +10745,9 @@ } }, "node_modules/postcss-import": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz", - "integrity": "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz", + "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11143,9 +11104,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11178,30 +11139,29 @@ "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", "dev": true, - "license": "MIT", "peerDependencies": { "typescript": ">= 4.3.x" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-i18next": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz", - "integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz", + "integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.0", + "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { @@ -11873,9 +11833,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", - "dev": true, - "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -11884,9 +11841,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", - "dev": true, - "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12026,7 +11980,6 @@ "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", @@ -12050,13 +12003,13 @@ } }, "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/source-map-js": { @@ -12111,17 +12064,18 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.0.8.tgz", - "integrity": "sha512-GlOB3HAtzRYc237+o46nnETNkc2Qckh3UrIJ1rJyAzagIlPWau/jTxjSz76sqRODEnt01m8CyIkw3PGv0q1UpQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.1.tgz", + "integrity": "sha512-q6GaGZdVZh6rjOdGnc+4hGTu8ECyhyjQDw4EZNxKtQjDO8kqtuxbFm8l/IP2l+zLVJAatGWKkaX9Qcd7QZxz+Q==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", - "@vitest/expect": "3.0.9", - "@vitest/spy": "3.0.9", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/spy": "3.2.4", "better-opn": "^3.0.2", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", "esbuild-register": "^3.5.0", @@ -12159,9 +12113,9 @@ } }, "node_modules/storybook-react-i18next": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/storybook-react-i18next/-/storybook-react-i18next-4.0.7.tgz", - "integrity": "sha512-rYmV1UObKoWe8vqo8JCLFfHLGn7dIf5HE+NCBTmRpFcfRrLbSRCCkM0dPCT2T8m11RM562ceVT6Pa6B2Zw1UIw==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/storybook-react-i18next/-/storybook-react-i18next-4.0.11.tgz", + "integrity": "sha512-p6gcz8//n7mtBaP75yZx910/t9Z4aIwOP+xzCvxwTzWL19NT1YGTR4GyR0ybzbEebqlPJtJVHnpGKQQD4wRyYg==", "dev": true, "license": "MIT", "dependencies": { @@ -12171,7 +12125,7 @@ "i18next": "^22.0.0 || ^23.0.0 || ^24.0.0 || ^25.0.0", "i18next-browser-languagedetector": "^7.0.0 || ^8.0.0", "i18next-http-backend": "^2.0.0 || ^3.0.0", - "react": ">=16.8.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-i18next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", "storybook": "^9.0.0" } @@ -12401,9 +12355,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.24.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.24.1.tgz", - "integrity": "sha512-ITeWc7CCAfK53u8jnV39UNqStQZjSt+bVYtJHsOEL3vVj/WV9/8HmsF8Ej4oD8r+Xk1HpWyeW/t59r1QNeAcUQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.0.tgz", + "integrity": "sha512-tS6LRyBhY6yAqxrfsA9IYpGWPUJOri6sclySa7TdC7XQfGLvTwDY531KLgfQwHEtQsn+sT4JlUspbeQDBVGWig==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -12722,9 +12676,9 @@ } }, "node_modules/tinypool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", - "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -12742,11 +12696,10 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -13016,6 +12969,19 @@ "node": ">=14" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universal-user-agent": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", @@ -13425,9 +13391,9 @@ } }, "node_modules/vite-node": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", - "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -13497,20 +13463,20 @@ "license": "MIT" }, "node_modules/vitest": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", - "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.3", - "@vitest/mocker": "3.2.3", - "@vitest/pretty-format": "^3.2.3", - "@vitest/runner": "3.2.3", - "@vitest/snapshot": "3.2.3", - "@vitest/spy": "3.2.3", - "@vitest/utils": "3.2.3", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -13521,10 +13487,10 @@ "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", - "tinypool": "^1.1.0", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.3", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -13540,8 +13506,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.3", - "@vitest/ui": "3.2.3", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, @@ -13569,74 +13535,6 @@ } } }, - "node_modules/vitest/node_modules/@vitest/expect": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", - "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.3", - "@vitest/utils": "3.2.3", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", - "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/@vitest/spy": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", - "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/@vitest/utils": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", - "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.3", - "loupe": "^3.1.3", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e23e24218..a0152dcde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,59 +22,59 @@ "@fontsource/inter": "^5.2.6", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", - "@tanstack/react-query": "^5.80.7", - "@tanstack/react-router": "^1.121.2", - "@vector-im/compound-design-tokens": "4.0.4", - "@vector-im/compound-web": "^8.0.0", + "@tanstack/react-query": "^5.84.1", + "@tanstack/react-router": "^1.130.10", + "@vector-im/compound-design-tokens": "5.0.2", + "@vector-im/compound-web": "^8.2.0", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "classnames": "^2.5.1", "date-fns": "^4.1.0", - "i18next": "^25.2.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-i18next": "^15.5.2", - "swagger-ui-dist": "^5.24.1", + "i18next": "^25.3.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-i18next": "^15.6.1", + "swagger-ui-dist": "^5.27.0", "valibot": "^1.1.0", "vaul": "^1.1.2" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "^2.1.2", "@browser-logos/chrome": "^2.0.0", "@browser-logos/firefox": "^3.0.10", "@browser-logos/safari": "^2.1.0", "@codecov/vite-plugin": "^1.9.1", "@graphql-codegen/cli": "^5.0.7", - "@graphql-codegen/client-preset": "^4.8.2", + "@graphql-codegen/client-preset": "^4.8.3", "@graphql-codegen/typescript-msw": "^3.0.1", - "@storybook/addon-docs": "^9.0.8", - "@storybook/react-vite": "^9.0.8", - "@tanstack/react-query-devtools": "^5.80.7", - "@tanstack/react-router-devtools": "^1.121.5", - "@tanstack/router-plugin": "^1.121.4", - "@testing-library/jest-dom": "^6.6.3", + "@storybook/addon-docs": "^9.1.1", + "@storybook/react-vite": "^9.1.1", + "@tanstack/react-query-devtools": "^5.84.1", + "@tanstack/react-router-devtools": "^1.130.10", + "@tanstack/router-plugin": "^1.130.10", + "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.0.1", - "@types/react": "19.1.8", - "@types/react-dom": "19.1.6", - "@types/swagger-ui-dist": "^3.30.5", + "@types/node": "^24.1.0", + "@types/react": "19.1.9", + "@types/react-dom": "19.1.7", + "@types/swagger-ui-dist": "^3.30.6", "@vitejs/plugin-react": "^4.5.2", - "@vitest/coverage-v8": "^3.2.3", + "@vitest/coverage-v8": "^3.2.4", "autoprefixer": "^10.4.21", "browserslist-to-esbuild": "^2.1.1", "graphql": "^16.11.0", "happy-dom": "^18.0.1", "i18next-parser": "^9.3.0", - "knip": "^5.61.0", - "msw": "^2.10.2", + "knip": "^5.62.0", + "msw": "^2.10.4", "msw-storybook-addon": "^2.0.5", - "postcss": "^8.5.5", - "postcss-import": "^16.1.0", + "postcss": "^8.5.6", + "postcss-import": "^16.1.1", "postcss-nesting": "^13.0.2", "rimraf": "^6.0.1", "storybook": "^9.0.1", - "storybook-react-i18next": "4.0.7", + "storybook-react-i18next": "4.0.11", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "vite": "6.3.5", diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 0e71a519d..99da32010 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -886,7 +886,7 @@ type Mutation { """ lockUser(input: LockUserInput!): LockUserPayload! """ - Unlock a user. This is only available to administrators. + Unlock and reactivate a user. This is only available to administrators. """ unlockUser(input: UnlockUserInput!): UnlockUserPayload! """ diff --git a/frontend/src/@types/i18next.d.ts b/frontend/src/@types/i18next.d.ts index cf32447c2..def4ce458 100644 --- a/frontend/src/@types/i18next.d.ts +++ b/frontend/src/@types/i18next.d.ts @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import "i18next"; import type translation from "../../locales/en.json"; diff --git a/frontend/src/components/AccountDeleteButton.tsx b/frontend/src/components/AccountDeleteButton.tsx index cb42edc81..6f1b80bec 100644 --- a/frontend/src/components/AccountDeleteButton.tsx +++ b/frontend/src/components/AccountDeleteButton.tsx @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { useMutation } from "@tanstack/react-query"; import IconDelete from "@vector-im/compound-design-tokens/assets/web/icons/delete"; @@ -70,7 +70,10 @@ const AccountDeleteButton: React.FC = (props) => { mutationFn: ({ password, hsErase, - }: { password: string | null; hsErase: boolean }) => + }: { + password: string | null; + hsErase: boolean; + }) => graphqlRequest({ query: MUTATION, variables: { password, hsErase }, diff --git a/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.module.css b/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.module.css index 51a12c9b5..b2d613a67 100644 --- a/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.module.css +++ b/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .link { diff --git a/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.tsx b/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.tsx index 6bb9ad587..c52a61ee5 100644 --- a/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.tsx +++ b/frontend/src/components/AccountManagementPasswordPreview/AccountManagementPasswordPreview.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Link } from "@tanstack/react-router"; import { Form } from "@vector-im/compound-web"; diff --git a/frontend/src/components/AccountManagementPasswordPreview/index.ts b/frontend/src/components/AccountManagementPasswordPreview/index.ts index 8597fdffa..723421535 100644 --- a/frontend/src/components/AccountManagementPasswordPreview/index.ts +++ b/frontend/src/components/AccountManagementPasswordPreview/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./AccountManagementPasswordPreview"; diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx index 3b2e7a58a..e4cf861c8 100644 --- a/frontend/src/components/BrowserSession.tsx +++ b/frontend/src/components/BrowserSession.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import IconChrome from "@browser-logos/chrome/chrome_64x64.png?url"; import IconFirefox from "@browser-logos/firefox/firefox_64x64.png?url"; diff --git a/frontend/src/components/ButtonLink.module.css b/frontend/src/components/ButtonLink.module.css index 70300b40d..821188520 100644 --- a/frontend/src/components/ButtonLink.module.css +++ b/frontend/src/components/ButtonLink.module.css @@ -1,7 +1,7 @@ -/* Copyright 2024 New Vector Ltd. +/* Copyright 2024, 2025 New Vector Ltd. * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ /* The weird selector is to have higher specificity than compound-web's button-link */ diff --git a/frontend/src/components/ButtonLink.tsx b/frontend/src/components/ButtonLink.tsx index 2a0a6b8b8..57362612d 100644 --- a/frontend/src/components/ButtonLink.tsx +++ b/frontend/src/components/ButtonLink.tsx @@ -1,13 +1,13 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { createLink } from "@tanstack/react-router"; import { Button } from "@vector-im/compound-web"; import cx from "classnames"; -import { type PropsWithChildren, forwardRef } from "react"; +import { forwardRef, type PropsWithChildren } from "react"; import styles from "./ButtonLink.module.css"; type Props = { diff --git a/frontend/src/components/Client/OAuth2ClientDetail.test.tsx b/frontend/src/components/Client/OAuth2ClientDetail.test.tsx index 70a9a58f8..ba3e920a0 100644 --- a/frontend/src/components/Client/OAuth2ClientDetail.test.tsx +++ b/frontend/src/components/Client/OAuth2ClientDetail.test.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @vitest-environment happy-dom diff --git a/frontend/src/components/Client/OAuth2ClientDetail.tsx b/frontend/src/components/Client/OAuth2ClientDetail.tsx index 1c8d019c2..0720da560 100644 --- a/frontend/src/components/Client/OAuth2ClientDetail.tsx +++ b/frontend/src/components/Client/OAuth2ClientDetail.tsx @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { H3 } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/components/Collapsible/Collapsible.module.css b/frontend/src/components/Collapsible/Collapsible.module.css index 5da9b4ed8..f8aec80d9 100644 --- a/frontend/src/components/Collapsible/Collapsible.module.css +++ b/frontend/src/components/Collapsible/Collapsible.module.css @@ -1,8 +1,8 @@ /* Copyright 2024, 2025 New Vector Ltd. * Copyright 2024 The Matrix.org Foundation C.I.C. * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .root { diff --git a/frontend/src/components/Collapsible/Collapsible.stories.tsx b/frontend/src/components/Collapsible/Collapsible.stories.tsx index 392b98c14..95c7a8e7e 100644 --- a/frontend/src/components/Collapsible/Collapsible.stories.tsx +++ b/frontend/src/components/Collapsible/Collapsible.stories.tsx @@ -1,7 +1,7 @@ // Copyright 2025 New Vector Ltd. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; import * as Collapsible from "./Collapsible"; diff --git a/frontend/src/components/Collapsible/Collapsible.tsx b/frontend/src/components/Collapsible/Collapsible.tsx index fb3181445..67f585c7a 100644 --- a/frontend/src/components/Collapsible/Collapsible.tsx +++ b/frontend/src/components/Collapsible/Collapsible.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import * as Collapsible from "@radix-ui/react-collapsible"; import IconChevronUp from "@vector-im/compound-design-tokens/assets/web/icons/chevron-up"; diff --git a/frontend/src/components/Collapsible/index.ts b/frontend/src/components/Collapsible/index.ts index 70e50b537..c1e0960db 100644 --- a/frontend/src/components/Collapsible/index.ts +++ b/frontend/src/components/Collapsible/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export * from "./Collapsible"; diff --git a/frontend/src/components/CompatSession.test.tsx b/frontend/src/components/CompatSession.test.tsx index 8e48de140..35430c4ce 100644 --- a/frontend/src/components/CompatSession.test.tsx +++ b/frontend/src/components/CompatSession.test.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. -// Copyright 2023-2024 The Matrix.org Foundation C.I.C. +// Copyright 2024, 2025 New Vector Ltd. +// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @vitest-environment happy-dom diff --git a/frontend/src/components/CompatSession.tsx b/frontend/src/components/CompatSession.tsx index 2770993ad..c52295baf 100644 --- a/frontend/src/components/CompatSession.tsx +++ b/frontend/src/components/CompatSession.tsx @@ -1,8 +1,8 @@ // Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/components/DateTime.stories.tsx b/frontend/src/components/DateTime.stories.tsx index a561cf224..4ed54f7a6 100644 --- a/frontend/src/components/DateTime.stories.tsx +++ b/frontend/src/components/DateTime.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; import { sub } from "date-fns"; diff --git a/frontend/src/components/DateTime.tsx b/frontend/src/components/DateTime.tsx index 8a901cf84..bd7e904e2 100644 --- a/frontend/src/components/DateTime.tsx +++ b/frontend/src/components/DateTime.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { differenceInHours, diff --git a/frontend/src/components/Dialog/Dialog.module.css b/frontend/src/components/Dialog/Dialog.module.css index ac59007e9..eceb0c3bc 100644 --- a/frontend/src/components/Dialog/Dialog.module.css +++ b/frontend/src/components/Dialog/Dialog.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .overlay, @@ -95,6 +95,7 @@ /* Cap the block size */ max-block-size: calc(100vh - var(--cpd-space-4x)); + /* biome-ignore lint/suspicious/noDuplicateProperties: this isn't a real duplicate */ max-block-size: calc(100svh - var(--cpd-space-4x)); /* Drawer comes in the Android style by default */ diff --git a/frontend/src/components/Dialog/Dialog.stories.tsx b/frontend/src/components/Dialog/Dialog.stories.tsx index 2059288f5..a1732a712 100644 --- a/frontend/src/components/Dialog/Dialog.stories.tsx +++ b/frontend/src/components/Dialog/Dialog.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; import { action } from "storybook/actions"; diff --git a/frontend/src/components/Dialog/Dialog.tsx b/frontend/src/components/Dialog/Dialog.tsx index 7f227d24e..45dba57a2 100644 --- a/frontend/src/components/Dialog/Dialog.tsx +++ b/frontend/src/components/Dialog/Dialog.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Close, @@ -102,4 +102,4 @@ export const Title: React.FC = ({ children }) => ( {children} ); -export { Description, Close } from "@radix-ui/react-dialog"; +export { Close, Description } from "@radix-ui/react-dialog"; diff --git a/frontend/src/components/Dialog/index.ts b/frontend/src/components/Dialog/index.ts index 8ce19f439..73467a18a 100644 --- a/frontend/src/components/Dialog/index.ts +++ b/frontend/src/components/Dialog/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. -export { Close, Dialog, Title, Description } from "./Dialog"; +export { Close, Description, Dialog, Title } from "./Dialog"; diff --git a/frontend/src/components/EmptyState/EmptyState.module.css b/frontend/src/components/EmptyState/EmptyState.module.css index 7a882aab1..e83a56df5 100644 --- a/frontend/src/components/EmptyState/EmptyState.module.css +++ b/frontend/src/components/EmptyState/EmptyState.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .empty-state { diff --git a/frontend/src/components/EmptyState/EmptyState.stories.tsx b/frontend/src/components/EmptyState/EmptyState.stories.tsx index b153ce212..33b32b3a5 100644 --- a/frontend/src/components/EmptyState/EmptyState.stories.tsx +++ b/frontend/src/components/EmptyState/EmptyState.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; diff --git a/frontend/src/components/EmptyState/EmptyState.tsx b/frontend/src/components/EmptyState/EmptyState.tsx index dbeae0d75..b40e3e885 100644 --- a/frontend/src/components/EmptyState/EmptyState.tsx +++ b/frontend/src/components/EmptyState/EmptyState.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import classNames from "classnames"; import { forwardRef } from "react"; diff --git a/frontend/src/components/EmptyState/index.ts b/frontend/src/components/EmptyState/index.ts index dde4804fe..6acdbdfdd 100644 --- a/frontend/src/components/EmptyState/index.ts +++ b/frontend/src/components/EmptyState/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { EmptyState as default } from "./EmptyState"; diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index 3d8122a93..ad460f875 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { type ErrorInfo, PureComponent, type ReactNode } from "react"; diff --git a/frontend/src/components/ExternalLink/ExternalLink.module.css b/frontend/src/components/ExternalLink/ExternalLink.module.css index 0555143e0..2d02ce33e 100644 --- a/frontend/src/components/ExternalLink/ExternalLink.module.css +++ b/frontend/src/components/ExternalLink/ExternalLink.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .external-link { diff --git a/frontend/src/components/ExternalLink/ExternalLink.tsx b/frontend/src/components/ExternalLink/ExternalLink.tsx index 98865667e..7b75891f0 100644 --- a/frontend/src/components/ExternalLink/ExternalLink.tsx +++ b/frontend/src/components/ExternalLink/ExternalLink.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Link } from "@vector-im/compound-web"; import classNames from "classnames"; diff --git a/frontend/src/components/Filter/Filter.module.css b/frontend/src/components/Filter/Filter.module.css index e9c8561e9..6bd2e392f 100644 --- a/frontend/src/components/Filter/Filter.module.css +++ b/frontend/src/components/Filter/Filter.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .filter { diff --git a/frontend/src/components/Filter/Filter.stories.tsx b/frontend/src/components/Filter/Filter.stories.tsx index 1393020b3..a9d8fa3d8 100644 --- a/frontend/src/components/Filter/Filter.stories.tsx +++ b/frontend/src/components/Filter/Filter.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; diff --git a/frontend/src/components/Filter/Filter.tsx b/frontend/src/components/Filter/Filter.tsx index 187c75d5d..a7c10800f 100644 --- a/frontend/src/components/Filter/Filter.tsx +++ b/frontend/src/components/Filter/Filter.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { createLink } from "@tanstack/react-router"; import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; diff --git a/frontend/src/components/Filter/index.ts b/frontend/src/components/Filter/index.ts index 521f0b4b4..e544ba38b 100644 --- a/frontend/src/components/Filter/index.ts +++ b/frontend/src/components/Filter/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { Filter as default } from "./Filter"; diff --git a/frontend/src/components/Footer/Footer.module.css b/frontend/src/components/Footer/Footer.module.css index 43f80286c..65b482986 100644 --- a/frontend/src/components/Footer/Footer.module.css +++ b/frontend/src/components/Footer/Footer.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .legal-footer { diff --git a/frontend/src/components/Footer/Footer.stories.tsx b/frontend/src/components/Footer/Footer.stories.tsx index 812f9a9f2..82e01a93b 100644 --- a/frontend/src/components/Footer/Footer.stories.tsx +++ b/frontend/src/components/Footer/Footer.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx index 7fbbc9d2d..d6e05d2ad 100644 --- a/frontend/src/components/Footer/Footer.tsx +++ b/frontend/src/components/Footer/Footer.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Link } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/components/Footer/index.ts b/frontend/src/components/Footer/index.ts index eb0958197..7db230d90 100644 --- a/frontend/src/components/Footer/index.ts +++ b/frontend/src/components/Footer/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./Footer"; diff --git a/frontend/src/components/GenericError.module.css b/frontend/src/components/GenericError.module.css index 7d7d417b6..e349b24c8 100644 --- a/frontend/src/components/GenericError.module.css +++ b/frontend/src/components/GenericError.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .details { diff --git a/frontend/src/components/GenericError.tsx b/frontend/src/components/GenericError.tsx index c54a5b049..c12bd70c5 100644 --- a/frontend/src/components/GenericError.tsx +++ b/frontend/src/components/GenericError.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import IconErrorSolid from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import { Button } from "@vector-im/compound-web"; diff --git a/frontend/src/components/Layout/Layout.module.css b/frontend/src/components/Layout/Layout.module.css index 04f36211e..794f16783 100644 --- a/frontend/src/components/Layout/Layout.module.css +++ b/frontend/src/components/Layout/Layout.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .layout-container { @@ -16,6 +16,7 @@ /* Fallback for browsers that do not support 100svh */ min-height: 100vh; + /* biome-ignore lint/suspicious/noDuplicateProperties: this isn't a real duplicate */ min-height: 100svh; margin: 0 auto; diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx index 207478cc7..039bee28a 100644 --- a/frontend/src/components/Layout/Layout.tsx +++ b/frontend/src/components/Layout/Layout.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; import cx from "classnames"; diff --git a/frontend/src/components/Layout/index.ts b/frontend/src/components/Layout/index.ts index 303e6cf06..042bd5da2 100644 --- a/frontend/src/components/Layout/index.ts +++ b/frontend/src/components/Layout/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default, query } from "./Layout"; diff --git a/frontend/src/components/Link.tsx b/frontend/src/components/Link.tsx index d9b791ae3..8dabc17f9 100644 --- a/frontend/src/components/Link.tsx +++ b/frontend/src/components/Link.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { createLink } from "@tanstack/react-router"; import { Link as CompoundLink } from "@vector-im/compound-web"; diff --git a/frontend/src/components/LoadingScreen/LoadingScreen.module.css b/frontend/src/components/LoadingScreen/LoadingScreen.module.css index b3be023a1..5963f9ecc 100644 --- a/frontend/src/components/LoadingScreen/LoadingScreen.module.css +++ b/frontend/src/components/LoadingScreen/LoadingScreen.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .loading-screen { @@ -10,6 +10,7 @@ /* Fallback for browsers that do not support 100svh */ min-height: 100vh; + /* biome-ignore lint/suspicious/noDuplicateProperties: this isn't a real duplicate */ min-height: 100svh; justify-content: center; diff --git a/frontend/src/components/LoadingScreen/LoadingScreen.stories.tsx b/frontend/src/components/LoadingScreen/LoadingScreen.stories.tsx index d794b4c07..3c018c4ab 100644 --- a/frontend/src/components/LoadingScreen/LoadingScreen.stories.tsx +++ b/frontend/src/components/LoadingScreen/LoadingScreen.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; diff --git a/frontend/src/components/LoadingScreen/LoadingScreen.test.tsx b/frontend/src/components/LoadingScreen/LoadingScreen.test.tsx index b4fdae354..b5b3b8219 100644 --- a/frontend/src/components/LoadingScreen/LoadingScreen.test.tsx +++ b/frontend/src/components/LoadingScreen/LoadingScreen.test.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @vitest-environment happy-dom diff --git a/frontend/src/components/LoadingScreen/LoadingScreen.tsx b/frontend/src/components/LoadingScreen/LoadingScreen.tsx index 6195ce4b8..a514470ba 100644 --- a/frontend/src/components/LoadingScreen/LoadingScreen.tsx +++ b/frontend/src/components/LoadingScreen/LoadingScreen.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import LoadingSpinner from "../LoadingSpinner"; diff --git a/frontend/src/components/LoadingScreen/index.ts b/frontend/src/components/LoadingScreen/index.ts index 31f80223f..d7b64615b 100644 --- a/frontend/src/components/LoadingScreen/index.ts +++ b/frontend/src/components/LoadingScreen/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./LoadingScreen"; diff --git a/frontend/src/components/LoadingSpinner/LoadingSpinner.module.css b/frontend/src/components/LoadingSpinner/LoadingSpinner.module.css index cb15b455a..48fe727b2 100644 --- a/frontend/src/components/LoadingSpinner/LoadingSpinner.module.css +++ b/frontend/src/components/LoadingSpinner/LoadingSpinner.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ @keyframes spin { diff --git a/frontend/src/components/LoadingSpinner/LoadingSpinner.stories.tsx b/frontend/src/components/LoadingSpinner/LoadingSpinner.stories.tsx index c175984ec..33e978698 100644 --- a/frontend/src/components/LoadingSpinner/LoadingSpinner.stories.tsx +++ b/frontend/src/components/LoadingSpinner/LoadingSpinner.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; diff --git a/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx index 96a879d37..8177b87a6 100644 --- a/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx +++ b/frontend/src/components/LoadingSpinner/LoadingSpinner.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import cx from "classnames"; import { useTranslation } from "react-i18next"; diff --git a/frontend/src/components/LoadingSpinner/index.ts b/frontend/src/components/LoadingSpinner/index.ts index cbc3a1932..72580588e 100644 --- a/frontend/src/components/LoadingSpinner/index.ts +++ b/frontend/src/components/LoadingSpinner/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./LoadingSpinner"; diff --git a/frontend/src/components/NavBar/NavBar.module.css b/frontend/src/components/NavBar/NavBar.module.css index dc4309efa..59a79706f 100644 --- a/frontend/src/components/NavBar/NavBar.module.css +++ b/frontend/src/components/NavBar/NavBar.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .nav-bar { diff --git a/frontend/src/components/NavBar/NavBar.stories.tsx b/frontend/src/components/NavBar/NavBar.stories.tsx index 831ea6617..194007c13 100644 --- a/frontend/src/components/NavBar/NavBar.stories.tsx +++ b/frontend/src/components/NavBar/NavBar.stories.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import type { Meta, StoryObj } from "@storybook/react-vite"; diff --git a/frontend/src/components/NavBar/NavBar.tsx b/frontend/src/components/NavBar/NavBar.tsx index 7751b509b..e35bf76a0 100644 --- a/frontend/src/components/NavBar/NavBar.tsx +++ b/frontend/src/components/NavBar/NavBar.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import styles from "./NavBar.module.css"; diff --git a/frontend/src/components/NavBar/index.ts b/frontend/src/components/NavBar/index.ts index 98b14f2c1..e8b54d06a 100644 --- a/frontend/src/components/NavBar/index.ts +++ b/frontend/src/components/NavBar/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./NavBar"; diff --git a/frontend/src/components/NavItem/NavItem.module.css b/frontend/src/components/NavItem/NavItem.module.css index 45f354ca7..a07515e16 100644 --- a/frontend/src/components/NavItem/NavItem.module.css +++ b/frontend/src/components/NavItem/NavItem.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2023, 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2023, 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .nav-tab { diff --git a/frontend/src/components/NavItem/NavItem.tsx b/frontend/src/components/NavItem/NavItem.tsx index 404425227..9af4d98fa 100644 --- a/frontend/src/components/NavItem/NavItem.tsx +++ b/frontend/src/components/NavItem/NavItem.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2022-2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Link } from "@tanstack/react-router"; diff --git a/frontend/src/components/NavItem/index.ts b/frontend/src/components/NavItem/index.ts index 653f04a4f..91f7b6ed8 100644 --- a/frontend/src/components/NavItem/index.ts +++ b/frontend/src/components/NavItem/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./NavItem"; diff --git a/frontend/src/components/NotFound.tsx b/frontend/src/components/NotFound.tsx index e56852d78..ab92f25a5 100644 --- a/frontend/src/components/NotFound.tsx +++ b/frontend/src/components/NotFound.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Alert } from "@vector-im/compound-web"; import type { ReactNode } from "react"; diff --git a/frontend/src/components/OAuth2Session.test.tsx b/frontend/src/components/OAuth2Session.test.tsx index 9d6d840b2..575e4a2ab 100644 --- a/frontend/src/components/OAuth2Session.test.tsx +++ b/frontend/src/components/OAuth2Session.test.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // @vitest-environment happy-dom diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx index a72fa4aba..3cac3a399 100644 --- a/frontend/src/components/OAuth2Session.tsx +++ b/frontend/src/components/OAuth2Session.tsx @@ -1,3 +1,8 @@ +// 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. + import { parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; import { type FragmentType, graphql, useFragment } from "../gql"; diff --git a/frontend/src/components/PageHeading/PageHeading.module.css b/frontend/src/components/PageHeading/PageHeading.module.css index df8ed4900..42e0b4774 100644 --- a/frontend/src/components/PageHeading/PageHeading.module.css +++ b/frontend/src/components/PageHeading/PageHeading.module.css @@ -1,8 +1,8 @@ -/* Copyright 2024 New Vector Ltd. -* Copyright 2024 The Matrix.org Foundation C.I.C. -* -* SPDX-License-Identifier: AGPL-3.0-only -* Please see LICENSE in the repository root for full details. +/* Copyright 2024, 2025 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. */ .page-heading { diff --git a/frontend/src/components/PageHeading/PageHeading.tsx b/frontend/src/components/PageHeading/PageHeading.tsx index 72efbb1e0..1d2436472 100644 --- a/frontend/src/components/PageHeading/PageHeading.tsx +++ b/frontend/src/components/PageHeading/PageHeading.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import cx from "classnames"; diff --git a/frontend/src/components/PageHeading/index.ts b/frontend/src/components/PageHeading/index.ts index 0c481b4a2..4a572d8fe 100644 --- a/frontend/src/components/PageHeading/index.ts +++ b/frontend/src/components/PageHeading/index.ts @@ -1,7 +1,7 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. export { default } from "./PageHeading"; diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx index dc198152e..f8d3e68c3 100644 --- a/frontend/src/components/PaginationControls.tsx +++ b/frontend/src/components/PaginationControls.tsx @@ -1,8 +1,8 @@ -// Copyright 2024 New Vector Ltd. +// Copyright 2024, 2025 New Vector Ltd. // Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // -// SPDX-License-Identifier: AGPL-3.0-only -// Please see LICENSE in the repository root for full details. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. import { Button } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; @@ -39,9 +39,9 @@ const PaginationControls: React.FC = ({ {t("common.previous")}
- {count !== undefined ? ( - <>{t("frontend.pagination_controls.total", { totalCount: count })} - ) : null} + {count !== undefined + ? t("frontend.pagination_controls.total", { totalCount: count }) + : null}