1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
Richard van der Hoff 2f6cf12255 fixes 2024-04-23 12:05:37 +01:00
Richard van der Hoff 769e9b11cf complement.sh: run tests from all test packages
... to save us remembering to add them to the list each time a new one is
added.
2024-04-23 11:42:20 +01:00
103 changed files with 1128 additions and 2934 deletions
+30
View File
@@ -85,3 +85,33 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book
destination_dir: ./${{ needs.pre.outputs.branch-version }}
################################################################################
pages-devdocs:
name: GitHub Pages (developer docs)
runs-on: ubuntu-latest
needs:
- pre
steps:
- uses: actions/checkout@v4
- name: "Set up Sphinx"
uses: matrix-org/setup-python-poetry@v1
with:
python-version: "3.x"
poetry-version: "1.3.2"
groups: "dev-docs"
extras: ""
- name: Build the documentation
run: |
cd dev-docs
poetry run make html
# Deploy to the target directory.
- name: Deploy to gh pages
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dev-docs/_build/html
destination_dir: ./dev-docs/${{ needs.pre.outputs.branch-version }}
-128
View File
@@ -1,131 +1,3 @@
Synapse 1.107.0 (2024-05-14)
============================
No significant changes since 1.107.0rc1.
# Synapse 1.107.0rc1 (2024-05-07)
### Features
- Add preliminary support for [MSC3823: Account Suspension](https://github.com/matrix-org/matrix-spec-proposals/pull/3823). ([\#17051](https://github.com/element-hq/synapse/issues/17051))
- Declare support for [Matrix v1.10](https://matrix.org/blog/2024/03/22/matrix-v1.10-release/). Contributed by @clokep. ([\#17082](https://github.com/element-hq/synapse/issues/17082))
- Add support for [MSC4115: membership metadata on events](https://github.com/matrix-org/matrix-spec-proposals/pull/4115). ([\#17104](https://github.com/element-hq/synapse/issues/17104), [\#17137](https://github.com/element-hq/synapse/issues/17137))
### Bugfixes
- Fixed search feature of Element Android on homesevers using SQLite by returning search terms as search highlights. ([\#17000](https://github.com/element-hq/synapse/issues/17000))
- Fixes a bug introduced in v1.52.0 where the `destination` query parameter for the [Destination Rooms Admin API](https://element-hq.github.io/synapse/v1.105/usage/administration/admin_api/federation.html#destination-rooms) failed to actually filter returned rooms. ([\#17077](https://github.com/element-hq/synapse/issues/17077))
- For MSC3266 room summaries, support queries at the recommended endpoint of `/_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`. The existing endpoint of `/_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary` is deprecated. ([\#17078](https://github.com/element-hq/synapse/issues/17078))
- Apply user email & picture during OIDC registration if present & selected. ([\#17120](https://github.com/element-hq/synapse/issues/17120))
- Improve error message for cross signing reset with [MSC3861](https://github.com/matrix-org/matrix-spec-proposals/pull/3861) enabled. ([\#17121](https://github.com/element-hq/synapse/issues/17121))
- Fix a bug which meant that to-device messages received over federation could be dropped when the server was under load or networking problems caused problems between Synapse processes or the database. ([\#17127](https://github.com/element-hq/synapse/issues/17127))
- Fix bug where `StreamChangeCache` would not respect configured cache factors. ([\#17152](https://github.com/element-hq/synapse/issues/17152))
### Updates to the Docker image
- Correct licensing metadata on Docker image. ([\#17141](https://github.com/element-hq/synapse/issues/17141))
### Improved Documentation
- Update the `event_cache_size` and `global_factor` configuration options' documentation. ([\#17071](https://github.com/element-hq/synapse/issues/17071))
- Remove broken sphinx docs. ([\#17073](https://github.com/element-hq/synapse/issues/17073), [\#17148](https://github.com/element-hq/synapse/issues/17148))
- Add RuntimeDirectory to example matrix-synapse.service systemd unit. ([\#17084](https://github.com/element-hq/synapse/issues/17084))
- Fix various small typos throughout the docs. ([\#17114](https://github.com/element-hq/synapse/issues/17114))
- Update enable_notifs configuration documentation. ([\#17116](https://github.com/element-hq/synapse/issues/17116))
- Update the Upgrade Notes with the latest minimum supported Rust version of 1.66.0. Contributed by @jahway603. ([\#17140](https://github.com/element-hq/synapse/issues/17140))
### Internal Changes
- Enable [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266) by default in the Synapse Complement image. ([\#17105](https://github.com/element-hq/synapse/issues/17105))
- Add optimisation to `StreamChangeCache.get_entities_changed(..)`. ([\#17130](https://github.com/element-hq/synapse/issues/17130))
### Updates to locked dependencies
* Bump furo from 2024.1.29 to 2024.4.27. ([\#17133](https://github.com/element-hq/synapse/issues/17133))
* Bump idna from 3.6 to 3.7. ([\#17136](https://github.com/element-hq/synapse/issues/17136))
* Bump jsonschema from 4.21.1 to 4.22.0. ([\#17157](https://github.com/element-hq/synapse/issues/17157))
* Bump lxml from 5.1.0 to 5.2.1. ([\#17158](https://github.com/element-hq/synapse/issues/17158))
* Bump phonenumbers from 8.13.29 to 8.13.35. ([\#17106](https://github.com/element-hq/synapse/issues/17106))
- Bump pillow from 10.2.0 to 10.3.0. ([\#17146](https://github.com/element-hq/synapse/issues/17146))
* Bump pydantic from 2.6.4 to 2.7.0. ([\#17107](https://github.com/element-hq/synapse/issues/17107))
* Bump pydantic from 2.7.0 to 2.7.1. ([\#17160](https://github.com/element-hq/synapse/issues/17160))
* Bump pyicu from 2.12 to 2.13. ([\#17109](https://github.com/element-hq/synapse/issues/17109))
* Bump serde from 1.0.197 to 1.0.198. ([\#17111](https://github.com/element-hq/synapse/issues/17111))
* Bump serde from 1.0.198 to 1.0.199. ([\#17132](https://github.com/element-hq/synapse/issues/17132))
* Bump serde from 1.0.199 to 1.0.200. ([\#17161](https://github.com/element-hq/synapse/issues/17161))
* Bump serde_json from 1.0.115 to 1.0.116. ([\#17112](https://github.com/element-hq/synapse/issues/17112))
- Update `tornado` Python dependency from 6.2 to 6.4. ([\#17131](https://github.com/element-hq/synapse/issues/17131))
* Bump twisted from 23.10.0 to 24.3.0. ([\#17135](https://github.com/element-hq/synapse/issues/17135))
* Bump types-bleach from 6.1.0.1 to 6.1.0.20240331. ([\#17110](https://github.com/element-hq/synapse/issues/17110))
* Bump types-pillow from 10.2.0.20240415 to 10.2.0.20240423. ([\#17159](https://github.com/element-hq/synapse/issues/17159))
* Bump types-setuptools from 69.0.0.20240125 to 69.5.0.20240423. ([\#17134](https://github.com/element-hq/synapse/issues/17134))
# Synapse 1.106.0 (2024-04-30)
No significant changes since 1.106.0rc1.
# Synapse 1.106.0rc1 (2024-04-25)
### Features
- Send an email if the address is already bound to an user account. ([\#16819](https://github.com/element-hq/synapse/issues/16819))
- Implement the rendezvous mechanism described by [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/issues/4108). ([\#17056](https://github.com/element-hq/synapse/issues/17056))
- Support delegating the rendezvous mechanism described [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/issues/4108) to an external implementation. ([\#17086](https://github.com/element-hq/synapse/issues/17086))
### Bugfixes
- Add validation to ensure that the `limit` parameter on `/publicRooms` is non-negative. ([\#16920](https://github.com/element-hq/synapse/issues/16920))
- Return `400 M_NOT_JSON` upon receiving invalid JSON in query parameters across various client and admin endpoints, rather than an internal server error. ([\#16923](https://github.com/element-hq/synapse/issues/16923))
- Make the CSAPI endpoint `/keys/device_signing/upload` idempotent. ([\#16943](https://github.com/element-hq/synapse/issues/16943))
- Redact membership events if the user requested erasure upon deactivating. ([\#17076](https://github.com/element-hq/synapse/issues/17076))
### Improved Documentation
- Add a prompt in the contributing guide to manually configure icu4c. ([\#17069](https://github.com/element-hq/synapse/issues/17069))
- Clarify what part of message retention is still experimental. ([\#17099](https://github.com/element-hq/synapse/issues/17099))
### Internal Changes
- Use new receipts column to optimise receipt and push action SQL queries. Contributed by Nick @ Beeper (@fizzadar). ([\#17032](https://github.com/element-hq/synapse/issues/17032), [\#17096](https://github.com/element-hq/synapse/issues/17096))
- Fix mypy with latest Twisted release. ([\#17036](https://github.com/element-hq/synapse/issues/17036))
- Bump minimum supported Rust version to 1.66.0. ([\#17079](https://github.com/element-hq/synapse/issues/17079))
- Add helpers to transform Twisted requests to Rust http Requests/Responses. ([\#17081](https://github.com/element-hq/synapse/issues/17081))
- Fix type annotation for `visited_chains` after `mypy` upgrade. ([\#17125](https://github.com/element-hq/synapse/issues/17125))
### Updates to locked dependencies
* Bump anyhow from 1.0.81 to 1.0.82. ([\#17095](https://github.com/element-hq/synapse/issues/17095))
* Bump peaceiris/actions-gh-pages from 3.9.3 to 4.0.0. ([\#17087](https://github.com/element-hq/synapse/issues/17087))
* Bump peaceiris/actions-mdbook from 1.2.0 to 2.0.0. ([\#17089](https://github.com/element-hq/synapse/issues/17089))
* Bump pyasn1-modules from 0.3.0 to 0.4.0. ([\#17093](https://github.com/element-hq/synapse/issues/17093))
* Bump pygithub from 2.2.0 to 2.3.0. ([\#17092](https://github.com/element-hq/synapse/issues/17092))
* Bump ruff from 0.3.5 to 0.3.7. ([\#17094](https://github.com/element-hq/synapse/issues/17094))
* Bump sigstore/cosign-installer from 3.4.0 to 3.5.0. ([\#17088](https://github.com/element-hq/synapse/issues/17088))
* Bump twine from 4.0.2 to 5.0.0. ([\#17091](https://github.com/element-hq/synapse/issues/17091))
* Bump types-pillow from 10.2.0.20240406 to 10.2.0.20240415. ([\#17090](https://github.com/element-hq/synapse/issues/17090))
# Synapse 1.105.1 (2024-04-23)
## Security advisory
The following issues are fixed in 1.105.1.
- [GHSA-3h7q-rfh9-xm4v](https://github.com/element-hq/synapse/security/advisories/GHSA-3h7q-rfh9-xm4v) / [CVE-2024-31208](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-31208) — High Severity
Weakness in auth chain indexing allows DoS from remote room members through disk fill and high CPU usage.
See the advisories for more details. If you have any questions, email security@element.io.
# Synapse 1.105.0 (2024-04-16)
No significant changes since 1.105.0rc1.
Generated
+8 -168
View File
@@ -59,12 +59,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
version = "1.6.0"
@@ -98,9 +92,9 @@ dependencies = [
[[package]]
name = "digest"
version = "0.10.7"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [
"block-buffer",
"crypto-common",
@@ -123,19 +117,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "headers"
version = "0.4.0"
@@ -201,15 +182,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -294,12 +266,6 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.76"
@@ -403,36 +369,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -485,18 +421,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.200"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.200"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
@@ -505,9 +441,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
@@ -525,17 +461,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "smallvec"
version = "1.10.0"
@@ -564,7 +489,6 @@ name = "synapse"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"blake2",
"bytes",
"headers",
@@ -572,15 +496,12 @@ dependencies = [
"http",
"lazy_static",
"log",
"mime",
"pyo3",
"pyo3-log",
"pythonize",
"regex",
"serde",
"serde_json",
"sha2",
"ulid",
]
[[package]]
@@ -595,17 +516,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "ulid"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259"
dependencies = [
"getrandom",
"rand",
"web-time",
]
[[package]]
name = "unicode-ident"
version = "1.0.5"
@@ -624,76 +534,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "windows-sys"
version = "0.36.1"
+1
View File
@@ -0,0 +1 @@
Adds validation to ensure that the `limit` parameter on `/publicRooms` is non-negative.
+1
View File
@@ -0,0 +1 @@
Return `400 M_NOT_JSON` upon receiving invalid JSON in query parameters across various client and admin endpoints, rather than an internal server error.
+1
View File
@@ -0,0 +1 @@
Make the CSAPI endpoint `/keys/device_signing/upload` idempotent.
+1
View File
@@ -0,0 +1 @@
Use new receipts column to optimise receipt and push action SQL queries. Contributed by Nick @ Beeper (@fizzadar).
+1
View File
@@ -0,0 +1 @@
Fix mypy with latest Twisted release.
+1
View File
@@ -0,0 +1 @@
Add a prompt in the contributing guide to manually configure icu4c.
+1
View File
@@ -0,0 +1 @@
Bump minimum supported Rust version to 1.66.0.
+1
View File
@@ -0,0 +1 @@
Add helpers to transform Twisted requests to Rust http Requests/Responses.
+1
View File
@@ -0,0 +1 @@
Support delegating the rendezvous mechanism described MSC4108 to an external implementation.
+1
View File
@@ -0,0 +1 @@
Use new receipts column to optimise receipt and push action SQL queries. Contributed by Nick @ Beeper (@fizzadar).
+1
View File
@@ -0,0 +1 @@
Clarify what part of message retention is still experimental.
+1
View File
@@ -0,0 +1 @@
`complement.sh`: run tests from all test packages.
-30
View File
@@ -1,33 +1,3 @@
matrix-synapse-py3 (1.107.0) stable; urgency=medium
* New Synapse release 1.107.0.
-- Synapse Packaging team <packages@matrix.org> Tue, 14 May 2024 14:15:34 +0100
matrix-synapse-py3 (1.107.0~rc1) stable; urgency=medium
* New Synapse release 1.107.0rc1.
-- Synapse Packaging team <packages@matrix.org> Tue, 07 May 2024 16:26:26 +0100
matrix-synapse-py3 (1.106.0) stable; urgency=medium
* New Synapse release 1.106.0.
-- Synapse Packaging team <packages@matrix.org> Tue, 30 Apr 2024 11:51:43 +0100
matrix-synapse-py3 (1.106.0~rc1) stable; urgency=medium
* New Synapse release 1.106.0rc1.
-- Synapse Packaging team <packages@matrix.org> Thu, 25 Apr 2024 15:54:59 +0100
matrix-synapse-py3 (1.105.1) stable; urgency=medium
* New Synapse release 1.105.1.
-- Synapse Packaging team <packages@matrix.org> Tue, 23 Apr 2024 15:56:18 +0100
matrix-synapse-py3 (1.105.0) stable; urgency=medium
* New Synapse release 1.105.0.
+20
View File
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+50
View File
@@ -0,0 +1,50 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "Synapse development"
copyright = "2023, The Matrix.org Foundation C.I.C."
author = "The Synapse Maintainers and Community"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"autodoc2",
"myst_parser",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for Autodoc2 ----------------------------------------------------
autodoc2_docstring_parser_regexes = [
# this will render all docstrings as 'MyST' Markdown
(r".*", "myst"),
]
autodoc2_packages = [
{
"path": "../synapse",
# Don't render documentation for everything as a matter of course
"auto_mode": False,
},
]
# -- Options for MyST (Markdown) ---------------------------------------------
# myst_heading_anchors = 2
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_static_path = ["_static"]
+22
View File
@@ -0,0 +1,22 @@
.. Synapse Developer Documentation documentation master file, created by
sphinx-quickstart on Mon Mar 13 08:59:51 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to the Synapse Developer Documentation!
===========================================================
.. toctree::
:maxdepth: 2
:caption: Contents:
modules/federation_sender
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+5
View File
@@ -0,0 +1,5 @@
Federation Sender
=================
```{autodoc2-docstring} synapse.federation.sender
```
+1 -1
View File
@@ -163,7 +163,7 @@ FROM docker.io/library/python:${PYTHON_VERSION}-slim-bookworm
LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse'
LABEL org.opencontainers.image.documentation='https://github.com/element-hq/synapse/blob/master/docker/README.md'
LABEL org.opencontainers.image.source='https://github.com/element-hq/synapse.git'
LABEL org.opencontainers.image.licenses='AGPL-3.0-or-later'
LABEL org.opencontainers.image.licenses='Apache-2.0'
RUN \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
@@ -92,6 +92,8 @@ allow_device_name_lookup_over_federation: true
## Experimental Features ##
experimental_features:
# client-side support for partial state in /send_join responses
faster_joins: true
# Enable support for polls
msc3381_polls_enabled: true
# Enable deleting device-specific notification settings stored in account data
@@ -102,10 +104,6 @@ experimental_features:
msc3874_enabled: true
# no UIA for x-signing upload for the first time
msc3967_enabled: true
# Expose a room summary for public rooms
msc3266_enabled: true
msc4115_membership_on_events: true
server_notices:
system_mxid_localpart: _server
+1 -1
View File
@@ -1,6 +1,6 @@
# Edit Room Membership API
This API allows an administrator to join a user account with a given `user_id`
This API allows an administrator to join an user account with a given `user_id`
to a room with a given `room_id_or_alias`. You can only modify the membership of
local users. The server administrator must be in the room and have permission to
invite users.
+3 -3
View File
@@ -51,8 +51,8 @@ clients.
## Server configuration
Support for this feature can be enabled and configured by adding the
`retention` option in the Synapse configuration file (see
Support for this feature can be enabled and configured by adding a the
`retention` in the Synapse configuration file (see
[configuration manual](usage/configuration/config_documentation.md#retention)).
To enable support for message retention policies, set the setting
@@ -117,7 +117,7 @@ In this example, we define three jobs:
policy's `max_lifetime` is greater than a week.
Note that this example is tailored to show different configurations and
features slightly more jobs than is probably necessary (in practice, a
features slightly more jobs than it's probably necessary (in practice, a
server admin would probably consider it better to replace the two last
jobs with one that runs once a day and handles rooms which
policy's `max_lifetime` is greater than 3 days).
+1 -1
View File
@@ -128,7 +128,7 @@ can read more about that [here](https://www.postgresql.org/docs/10/kernel-resour
### Overview
The script `synapse_port_db` allows porting an existing synapse server
backed by SQLite to using PostgreSQL. This is done as a two phase
backed by SQLite to using PostgreSQL. This is done in as a two phase
process:
1. Copy the existing SQLite database to a separate location and run
+2 -2
View File
@@ -259,9 +259,9 @@ users, etc.) to the developers via the `--report-stats` argument.
This command will generate you a config file that you can then customise, but it will
also generate a set of keys for you. These keys will allow your homeserver to
identify itself to other homeservers, so don't lose or delete them. It would be
identify itself to other homeserver, so don't lose or delete them. It would be
wise to back them up somewhere safe. (If, for whatever reason, you do need to
change your homeserver's keys, you may find that other homeservers have the
change your homeserver's keys, you may find that other homeserver have the
old key cached. If you update the signing key, you should change the name of the
key in the `<server name>.signing.key` file (the second word) to something
different. See the [spec](https://matrix.org/docs/spec/server_server/latest.html#retrieving-server-keys) for more information on key management).
-1
View File
@@ -98,7 +98,6 @@ A custom mapping provider must specify the following methods:
either accept this localpart or pick their own username. Otherwise this
option has no effect. If omitted, defaults to `False`.
- `display_name`: An optional string, the display name for the user.
- `picture`: An optional string, the avatar url for the user.
- `emails`: A list of strings, the email address(es) to associate with
this user. If omitted, defaults to an empty list.
* `async def get_extra_attributes(self, userinfo, token)`
@@ -9,7 +9,6 @@ ReloadPropagatedFrom=matrix-synapse.target
Type=notify
NotifyAccess=main
User=matrix-synapse
RuntimeDirectory=synapse
WorkingDirectory=/var/lib/matrix-synapse
EnvironmentFile=-/etc/default/matrix-synapse
ExecStartPre=/opt/venvs/matrix-synapse/bin/python -m synapse.app.homeserver --config-path=/etc/matrix-synapse/homeserver.yaml --config-path=/etc/matrix-synapse/conf.d/ --generate-keys
-8
View File
@@ -117,14 +117,6 @@ each upgrade are complete before moving on to the next upgrade, to avoid
stacking them up. You can monitor the currently running background updates with
[the Admin API](usage/administration/admin_api/background_updates.html#status).
# Upgrading to v1.106.0
## Minimum supported Rust version
The minimum supported Rust version has been increased from v1.65.0 to v1.66.0.
Users building from source will need to ensure their `rustc` version is up to
date.
# Upgrading to v1.100.0
## Minimum supported Rust version
@@ -44,7 +44,7 @@ For each update:
## Enabled
This API allows pausing background updates.
This API allow pausing background updates.
Background updates should *not* be paused for significant periods of time, as
this can affect the performance of Synapse.
+1 -1
View File
@@ -241,7 +241,7 @@ in memory constrained environments, or increased if performance starts to
degrade.
However, degraded performance due to a low cache factor, common on
machines with slow disks, often leads to explosions in memory use due to
machines with slow disks, often leads to explosions in memory use due
backlogged requests. In this case, reducing the cache factor will make
things worse. Instead, try increasing it drastically. 2.0 is a good
starting value.
@@ -676,8 +676,8 @@ This setting has the following sub-options:
trailing 's'.
* `app_name`: `app_name` defines the default value for '%(app)s' in `notif_from` and email
subjects. It defaults to 'Matrix'.
* `enable_notifs`: Set to true to allow users to receive e-mail notifications. If this is not set,
users can configure e-mail notifications but will not receive them. Disabled by default.
* `enable_notifs`: Set to true to enable sending emails for messages that the user
has missed. Disabled by default.
* `notif_for_new_users`: Set to false to disable automatic subscription to email
notifications for new users. Enabled by default.
* `notif_delay_before_mail`: The time to wait before emailing about a notification.
@@ -1317,12 +1317,6 @@ Options related to caching.
The number of events to cache in memory. Defaults to 10K. Like other caches,
this is affected by `caches.global_factor` (see below).
For example, the default is 10K and the global_factor default is 0.5.
Since 10K * 0.5 is 5K then the event cache size will be 5K.
The cache affected by this configuration is named as "*getEvent*".
Note that this option is not part of the `caches` section.
Example configuration:
@@ -1348,8 +1342,6 @@ number of entries that can be stored.
Defaults to 0.5, which will halve the size of all caches.
Note that changing this value also affects the HTTP connection pool.
* `per_cache_factors`: A dictionary of cache name to cache factor for that individual
cache. Overrides the global cache factor for a given cache.
+2 -2
View File
@@ -86,9 +86,9 @@ The search term is then split into words:
* If unavailable, then runs of ASCII characters, numbers, underscores, and hyphens
are considered words.
The queries for PostgreSQL and SQLite are detailed below, but their overall goal
The queries for PostgreSQL and SQLite are detailed below, by their overall goal
is to find matching users, preferring users who are "real" (e.g. not bots,
not deactivated). It is assumed that real users will have a display name and
not deactivated). It is assumed that real users will have an display name and
avatar set.
### PostgreSQL
+2 -2
View File
@@ -232,7 +232,7 @@ information.
^/_matrix/client/v1/rooms/.*/hierarchy$
^/_matrix/client/(v1|unstable)/rooms/.*/relations/
^/_matrix/client/v1/rooms/.*/threads$
^/_matrix/client/unstable/im.nheko.summary/summary/.*$
^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary$
^/_matrix/client/(r0|v3|unstable)/account/3pid$
^/_matrix/client/(r0|v3|unstable)/account/whoami$
^/_matrix/client/(r0|v3|unstable)/devices$
@@ -634,7 +634,7 @@ worker application type.
#### Push Notifications
You can designate generic workers to send push notifications to
You can designate generic worker to sending push notifications to
a [push gateway](https://spec.matrix.org/v1.5/push-gateway-api/) such as
[sygnal](https://github.com/matrix-org/sygnal) and email.
Generated
+639 -366
View File
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -96,7 +96,7 @@ module-name = "synapse.synapse_rust"
[tool.poetry]
name = "matrix-synapse"
version = "1.107.0"
version = "1.105.0"
description = "Homeserver for the Matrix decentralised comms protocol"
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
license = "AGPL-3.0-or-later"
@@ -364,6 +364,17 @@ towncrier = ">=18.6.0rc1"
tomli = ">=1.2.3"
# Dependencies for building the development documentation
[tool.poetry.group.dev-docs]
optional = true
[tool.poetry.group.dev-docs.dependencies]
sphinx = {version = "^6.1", python = "^3.8"}
sphinx-autodoc2 = {version = ">=0.4.2,<0.6.0", python = "^3.8"}
myst-parser = {version = "^1.0.0", python = "^3.8"}
furo = ">=2022.12.7,<2025.0.0"
[build-system]
# The upper bounds here are defensive, intended to prevent situations like
# https://github.com/matrix-org/synapse/issues/13849 and
-4
View File
@@ -23,13 +23,11 @@ name = "synapse.synapse_rust"
[dependencies]
anyhow = "1.0.63"
base64 = "0.21.7"
bytes = "1.6.0"
headers = "0.4.0"
http = "1.1.0"
lazy_static = "1.4.0"
log = "0.4.17"
mime = "0.3.17"
pyo3 = { version = "0.20.0", features = [
"macros",
"anyhow",
@@ -39,10 +37,8 @@ pyo3 = { version = "0.20.0", features = [
pyo3-log = "0.9.0"
pythonize = "0.20.0"
regex = "1.6.0"
sha2 = "0.10.8"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
ulid = "1.1.2"
[features]
extension-module = ["pyo3/extension-module"]
+2 -7
View File
@@ -20,10 +20,8 @@
//! Implements the internal metadata class attached to events.
//!
//! The internal metadata is a bit like a `TypedDict`, in that most of
//! it is stored as a JSON dict in the DB (the exceptions being `outlier`
//! and `stream_ordering` which have their own columns in the database).
//! Most events have zero, or only a few, of these keys
//! The internal metadata is a bit like a `TypedDict`, in that it is stored as a
//! JSON dict in the DB. Most events have zero, or only a few, of these keys
//! set. Therefore, since we care more about memory size than performance here,
//! we store these fields in a mapping.
//!
@@ -236,9 +234,6 @@ impl EventInternalMetadata {
self.clone()
}
/// Get a dict holding the data stored in the `internal_metadata` column in the database.
///
/// Note that `outlier` and `stream_ordering` are stored in separate columns so are not returned here.
fn get_dict(&self, py: Python<'_>) -> PyResult<PyObject> {
let dict = PyDict::new(py);
-2
View File
@@ -7,7 +7,6 @@ pub mod errors;
pub mod events;
pub mod http;
pub mod push;
pub mod rendezvous;
lazy_static! {
static ref LOGGING_HANDLE: ResetHandle = pyo3_log::init();
@@ -46,7 +45,6 @@ fn synapse_rust(py: Python<'_>, m: &PyModule) -> PyResult<()> {
acl::register_module(py, m)?;
push::register_module(py, m)?;
events::register_module(py, m)?;
rendezvous::register_module(py, m)?;
Ok(())
}
-315
View File
@@ -1,315 +0,0 @@
/*
* This file is licensed under the Affero General Public License (AGPL) version 3.
*
* Copyright (C) 2024 New Vector, Ltd
*
* This program is free software: you can redistribute it and/or modify
* it 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.
*
* See the GNU Affero General Public License for more details:
* <https://www.gnu.org/licenses/agpl-3.0.html>.
*
*/
use std::{
collections::{BTreeMap, HashMap},
time::{Duration, SystemTime},
};
use bytes::Bytes;
use headers::{
AccessControlAllowOrigin, AccessControlExposeHeaders, CacheControl, ContentLength, ContentType,
HeaderMapExt, IfMatch, IfNoneMatch, Pragma,
};
use http::{header::ETAG, HeaderMap, Response, StatusCode, Uri};
use mime::Mime;
use pyo3::{
exceptions::PyValueError, pyclass, pymethods, types::PyModule, Py, PyAny, PyObject, PyResult,
Python, ToPyObject,
};
use ulid::Ulid;
use self::session::Session;
use crate::{
errors::{NotFoundError, SynapseError},
http::{http_request_from_twisted, http_response_to_twisted, HeaderMapPyExt},
};
mod session;
// n.b. Because OPTIONS requests are handled by the Python code, we don't need to set Access-Control-Allow-Headers.
fn prepare_headers(headers: &mut HeaderMap, session: &Session) {
headers.typed_insert(AccessControlAllowOrigin::ANY);
headers.typed_insert(AccessControlExposeHeaders::from_iter([ETAG]));
headers.typed_insert(Pragma::no_cache());
headers.typed_insert(CacheControl::new().with_no_store());
headers.typed_insert(session.etag());
headers.typed_insert(session.expires());
headers.typed_insert(session.last_modified());
}
#[pyclass]
struct RendezvousHandler {
base: Uri,
clock: PyObject,
sessions: BTreeMap<Ulid, Session>,
capacity: usize,
max_content_length: u64,
ttl: Duration,
}
impl RendezvousHandler {
/// Check the input headers of a request which sets data for a session, and return the content type.
fn check_input_headers(&self, headers: &HeaderMap) -> PyResult<Mime> {
let ContentLength(content_length) = headers.typed_get_required()?;
if content_length > self.max_content_length {
return Err(SynapseError::new(
StatusCode::PAYLOAD_TOO_LARGE,
"Payload too large".to_owned(),
"M_TOO_LARGE",
None,
None,
));
}
let content_type: ContentType = headers.typed_get_required()?;
// Content-Type must be text/plain
if content_type != ContentType::text() {
return Err(SynapseError::new(
StatusCode::BAD_REQUEST,
"Content-Type must be text/plain".to_owned(),
"M_INVALID_PARAM",
None,
None,
));
}
Ok(content_type.into())
}
/// Evict expired sessions and remove the oldest sessions until we're under the capacity.
fn evict(&mut self, now: SystemTime) {
// First remove all the entries which expired
self.sessions.retain(|_, session| !session.expired(now));
// Then we remove the oldest entires until we're under the limit
while self.sessions.len() > self.capacity {
self.sessions.pop_first();
}
}
}
#[pymethods]
impl RendezvousHandler {
#[new]
#[pyo3(signature = (homeserver, /, capacity=100, max_content_length=4*1024, eviction_interval=60*1000, ttl=60*1000))]
fn new(
py: Python<'_>,
homeserver: &PyAny,
capacity: usize,
max_content_length: u64,
eviction_interval: u64,
ttl: u64,
) -> PyResult<Py<Self>> {
let base: String = homeserver
.getattr("config")?
.getattr("server")?
.getattr("public_baseurl")?
.extract()?;
let base = Uri::try_from(format!("{base}_synapse/client/rendezvous"))
.map_err(|_| PyValueError::new_err("Invalid base URI"))?;
let clock = homeserver.call_method0("get_clock")?.to_object(py);
// Construct a Python object so that we can get a reference to the
// evict method and schedule it to run.
let self_ = Py::new(
py,
Self {
base,
clock,
sessions: BTreeMap::new(),
capacity,
max_content_length,
ttl: Duration::from_millis(ttl),
},
)?;
let evict = self_.getattr(py, "_evict")?;
homeserver.call_method0("get_clock")?.call_method(
"looping_call",
(evict, eviction_interval),
None,
)?;
Ok(self_)
}
fn _evict(&mut self, py: Python<'_>) -> PyResult<()> {
let clock = self.clock.as_ref(py);
let now: u64 = clock.call_method0("time_msec")?.extract()?;
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
self.evict(now);
Ok(())
}
fn handle_post(&mut self, py: Python<'_>, twisted_request: &PyAny) -> PyResult<()> {
let request = http_request_from_twisted(twisted_request)?;
let content_type = self.check_input_headers(request.headers())?;
let clock = self.clock.as_ref(py);
let now: u64 = clock.call_method0("time_msec")?.extract()?;
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
// We trigger an immediate eviction if we're at 2x the capacity
if self.sessions.len() >= self.capacity * 2 {
self.evict(now);
}
// Generate a new ULID for the session from the current time.
let id = Ulid::from_datetime(now);
let uri = format!("{base}/{id}", base = self.base);
let body = request.into_body();
let session = Session::new(body, content_type, now, self.ttl);
let response = serde_json::json!({
"url": uri,
})
.to_string();
let mut response = Response::new(response.as_bytes());
*response.status_mut() = StatusCode::CREATED;
response.headers_mut().typed_insert(ContentType::json());
prepare_headers(response.headers_mut(), &session);
http_response_to_twisted(twisted_request, response)?;
self.sessions.insert(id, session);
Ok(())
}
fn handle_get(&mut self, py: Python<'_>, twisted_request: &PyAny, id: &str) -> PyResult<()> {
let request = http_request_from_twisted(twisted_request)?;
let if_none_match: Option<IfNoneMatch> = request.headers().typed_get_optional()?;
let now: u64 = self.clock.call_method0(py, "time_msec")?.extract(py)?;
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
let session = self
.sessions
.get(&id)
.filter(|s| !s.expired(now))
.ok_or_else(NotFoundError::new)?;
if let Some(if_none_match) = if_none_match {
if !if_none_match.precondition_passes(&session.etag()) {
let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::NOT_MODIFIED;
prepare_headers(response.headers_mut(), session);
http_response_to_twisted(twisted_request, response)?;
return Ok(());
}
}
let mut response = Response::new(session.data());
*response.status_mut() = StatusCode::OK;
let headers = response.headers_mut();
prepare_headers(headers, session);
headers.typed_insert(session.content_type());
headers.typed_insert(session.content_length());
http_response_to_twisted(twisted_request, response)?;
Ok(())
}
fn handle_put(&mut self, py: Python<'_>, twisted_request: &PyAny, id: &str) -> PyResult<()> {
let request = http_request_from_twisted(twisted_request)?;
let content_type = self.check_input_headers(request.headers())?;
let if_match: IfMatch = request.headers().typed_get_required()?;
let data = request.into_body();
let now: u64 = self.clock.call_method0(py, "time_msec")?.extract(py)?;
let now = SystemTime::UNIX_EPOCH + Duration::from_millis(now);
let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
let session = self
.sessions
.get_mut(&id)
.filter(|s| !s.expired(now))
.ok_or_else(NotFoundError::new)?;
if !if_match.precondition_passes(&session.etag()) {
let mut headers = HeaderMap::new();
prepare_headers(&mut headers, session);
let mut additional_fields = HashMap::with_capacity(1);
additional_fields.insert(
String::from("org.matrix.msc4108.errcode"),
String::from("M_CONCURRENT_WRITE"),
);
return Err(SynapseError::new(
StatusCode::PRECONDITION_FAILED,
"ETag does not match".to_owned(),
"M_UNKNOWN", // Would be M_CONCURRENT_WRITE
Some(additional_fields),
Some(headers),
));
}
session.update(data, content_type, now);
let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::ACCEPTED;
prepare_headers(response.headers_mut(), session);
http_response_to_twisted(twisted_request, response)?;
Ok(())
}
fn handle_delete(&mut self, twisted_request: &PyAny, id: &str) -> PyResult<()> {
let _request = http_request_from_twisted(twisted_request)?;
let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
let _session = self.sessions.remove(&id).ok_or_else(NotFoundError::new)?;
let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::NO_CONTENT;
response
.headers_mut()
.typed_insert(AccessControlAllowOrigin::ANY);
http_response_to_twisted(twisted_request, response)?;
Ok(())
}
}
pub fn register_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
let child_module = PyModule::new(py, "rendezvous")?;
child_module.add_class::<RendezvousHandler>()?;
m.add_submodule(child_module)?;
// We need to manually add the module to sys.modules to make `from
// synapse.synapse_rust import rendezvous` work.
py.import("sys")?
.getattr("modules")?
.set_item("synapse.synapse_rust.rendezvous", child_module)?;
Ok(())
}
-91
View File
@@ -1,91 +0,0 @@
/*
* This file is licensed under the Affero General Public License (AGPL) version 3.
*
* Copyright (C) 2024 New Vector, Ltd
*
* This program is free software: you can redistribute it and/or modify
* it 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.
*
* See the GNU Affero General Public License for more details:
* <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
use std::time::{Duration, SystemTime};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use bytes::Bytes;
use headers::{ContentLength, ContentType, ETag, Expires, LastModified};
use mime::Mime;
use sha2::{Digest, Sha256};
/// A single session, containing data, metadata, and expiry information.
pub struct Session {
hash: [u8; 32],
data: Bytes,
content_type: Mime,
last_modified: SystemTime,
expires: SystemTime,
}
impl Session {
/// Create a new session with the given data, content type, and time-to-live.
pub fn new(data: Bytes, content_type: Mime, now: SystemTime, ttl: Duration) -> Self {
let hash = Sha256::digest(&data).into();
Self {
hash,
data,
content_type,
expires: now + ttl,
last_modified: now,
}
}
/// Returns true if the session has expired at the given time.
pub fn expired(&self, now: SystemTime) -> bool {
self.expires <= now
}
/// Update the session with new data, content type, and last modified time.
pub fn update(&mut self, data: Bytes, content_type: Mime, now: SystemTime) {
self.hash = Sha256::digest(&data).into();
self.data = data;
self.content_type = content_type;
self.last_modified = now;
}
/// Returns the Content-Type header of the session.
pub fn content_type(&self) -> ContentType {
self.content_type.clone().into()
}
/// Returns the Content-Length header of the session.
pub fn content_length(&self) -> ContentLength {
ContentLength(self.data.len() as _)
}
/// Returns the ETag header of the session.
pub fn etag(&self) -> ETag {
let encoded = URL_SAFE_NO_PAD.encode(self.hash);
// SAFETY: Base64 encoding is URL-safe, so ETag-safe
format!("\"{encoded}\"")
.parse()
.expect("base64-encoded hash should be URL-safe")
}
/// Returns the Last-Modified header of the session.
pub fn last_modified(&self) -> LastModified {
self.last_modified.into()
}
/// Returns the Expires header of the session.
pub fn expires(&self) -> Expires {
self.expires.into()
}
/// Returns the current data stored in the session.
pub fn data(&self) -> Bytes {
self.data.clone()
}
}
+6 -13
View File
@@ -214,18 +214,6 @@ fi
extra_test_args=()
test_packages=(
./tests/csapi
./tests
./tests/msc3874
./tests/msc3890
./tests/msc3391
./tests/msc3930
./tests/msc3902
./tests/msc3967
./tests/msc4115
)
# Enable dirty runs, so tests will reuse the same container where possible.
# This significantly speeds up tests, but increases the possibility of test pollution.
export COMPLEMENT_ENABLE_DIRTY_RUNS=1
@@ -288,7 +276,12 @@ fi
export PASS_SYNAPSE_LOG_TESTING=1
# Run the tests!
echo "Images built; running complement with ${extra_test_args[@]} $@ ${test_packages[@]}"
cd "$COMPLEMENT_DIR"
# This isn't whitespace-safe but *does* work on the prehistoric version of bash
# on OSX.
test_packages=( $(find ./tests -type d) )
echo "Images built; running complement with ${extra_test_args[@]} $@ ${test_packages[@]}"
go test -v -tags "synapse_blacklist" -count=1 "${extra_test_args[@]}" "$@" "${test_packages[@]}"
+1
View File
@@ -91,6 +91,7 @@ else
"synapse" "docker" "tests"
"scripts-dev"
"contrib" "synmark" "stubs" ".ci"
"dev-docs"
)
fi
fi
+1 -1
View File
@@ -127,7 +127,7 @@ BOOLEAN_COLUMNS = {
"redactions": ["have_censored"],
"room_stats_state": ["is_federatable"],
"rooms": ["is_public", "has_auth_chain_index"],
"users": ["shadow_banned", "approved", "locked", "suspended"],
"users": ["shadow_banned", "approved", "locked"],
"un_partial_stated_event_stream": ["rejection_status_changed"],
"users_who_share_rooms": ["share_private"],
"per_user_experimental_features": ["enabled"],
-7
View File
@@ -234,13 +234,6 @@ class EventContentFields:
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
class EventUnsignedContentFields:
"""Fields found inside the 'unsigned' data on events"""
# Requesting user's membership, per MSC4115
MSC4115_MEMBERSHIP: Final = "io.element.msc4115.membership"
class RoomTypes:
"""Understood values of the room_type field of m.room.create events."""
-12
View File
@@ -52,7 +52,6 @@ DEFAULT_SUBJECTS = {
"invite_from_person_to_space": "[%(app)s] %(person)s has invited you to join the %(space)s space on %(app)s...",
"password_reset": "[%(server_name)s] Password reset",
"email_validation": "[%(server_name)s] Validate your email",
"email_already_in_use": "[%(server_name)s] Email already in use",
}
LEGACY_TEMPLATE_DIR_WARNING = """
@@ -77,7 +76,6 @@ class EmailSubjectConfig:
invite_from_person_to_space: str
password_reset: str
email_validation: str
email_already_in_use: str
class EmailConfig(Config):
@@ -182,12 +180,6 @@ class EmailConfig(Config):
registration_template_text = email_config.get(
"registration_template_text", "registration.txt"
)
already_in_use_template_html = email_config.get(
"already_in_use_template_html", "already_in_use.html"
)
already_in_use_template_text = email_config.get(
"already_in_use_template_html", "already_in_use.txt"
)
add_threepid_template_html = email_config.get(
"add_threepid_template_html", "add_threepid.html"
)
@@ -223,8 +215,6 @@ class EmailConfig(Config):
self.email_password_reset_template_text,
self.email_registration_template_html,
self.email_registration_template_text,
self.email_already_in_use_template_html,
self.email_already_in_use_template_text,
self.email_add_threepid_template_html,
self.email_add_threepid_template_text,
self.email_password_reset_template_confirmation_html,
@@ -240,8 +230,6 @@ class EmailConfig(Config):
password_reset_template_text,
registration_template_html,
registration_template_text,
already_in_use_template_html,
already_in_use_template_text,
add_threepid_template_html,
add_threepid_template_text,
"password_reset_confirmation.html",
+1 -15
View File
@@ -413,26 +413,12 @@ class ExperimentalConfig(Config):
)
# MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code
self.msc4108_enabled = experimental.get("msc4108_enabled", False)
self.msc4108_delegation_endpoint: Optional[str] = experimental.get(
"msc4108_delegation_endpoint", None
)
if (
self.msc4108_enabled or self.msc4108_delegation_endpoint is not None
) and not self.msc3861.enabled:
if self.msc4108_delegation_endpoint is not None and not self.msc3861.enabled:
raise ConfigError(
"MSC4108 requires MSC3861 to be enabled",
("experimental", "msc4108_delegation_endpoint"),
)
if self.msc4108_delegation_endpoint is not None and self.msc4108_enabled:
raise ConfigError(
"You cannot have MSC4108 both enabled and delegated at the same time",
("experimental", "msc4108_delegation_endpoint"),
)
self.msc4115_membership_on_events = experimental.get(
"msc4115_membership_on_events", False
)
+5 -25
View File
@@ -49,7 +49,7 @@ from synapse.api.errors import Codes, SynapseError
from synapse.api.room_versions import RoomVersion
from synapse.types import JsonDict, Requester
from . import EventBase, make_event_from_dict
from . import EventBase
if TYPE_CHECKING:
from synapse.handlers.relations import BundledAggregations
@@ -82,14 +82,17 @@ def prune_event(event: EventBase) -> EventBase:
"""
pruned_event_dict = prune_event_dict(event.room_version, event.get_dict())
from . import make_event_from_dict
pruned_event = make_event_from_dict(
pruned_event_dict, event.room_version, event.internal_metadata.get_dict()
)
# Copy the bits of `internal_metadata` that aren't returned by `get_dict`
# copy the internal fields
pruned_event.internal_metadata.stream_ordering = (
event.internal_metadata.stream_ordering
)
pruned_event.internal_metadata.outlier = event.internal_metadata.outlier
# Mark the event as redacted
@@ -98,29 +101,6 @@ def prune_event(event: EventBase) -> EventBase:
return pruned_event
def clone_event(event: EventBase) -> EventBase:
"""Take a copy of the event.
This is mostly useful because it does a *shallow* copy of the `unsigned` data,
which means it can then be updated without corrupting the in-memory cache. Note that
other properties of the event, such as `content`, are *not* (currently) copied here.
"""
# XXX: We rely on at least one of `event.get_dict()` and `make_event_from_dict()`
# making a copy of `unsigned`. Currently, both do, though I don't really know why.
# Still, as long as they do, there's not much point doing yet another copy here.
new_event = make_event_from_dict(
event.get_dict(), event.room_version, event.internal_metadata.get_dict()
)
# Copy the bits of `internal_metadata` that aren't returned by `get_dict`.
new_event.internal_metadata.stream_ordering = (
event.internal_metadata.stream_ordering
)
new_event.internal_metadata.outlier = event.internal_metadata.outlier
return new_event
def prune_event_dict(room_version: RoomVersion, event_dict: JsonDict) -> JsonDict:
"""Redacts the event_dict in the same way as `prune_event`, except it
operates on dicts rather than event objects
+18 -26
View File
@@ -546,25 +546,7 @@ class FederationServer(FederationBase):
edu_type=edu_dict["edu_type"],
content=edu_dict["content"],
)
try:
await self.registry.on_edu(edu.edu_type, origin, edu.content)
except Exception:
# If there was an error handling the EDU, we must reject the
# transaction.
#
# Some EDU types (notably, to-device messages) are, despite their name,
# expected to be reliable; if we weren't able to do something with it,
# we have to tell the sender that, and the only way the protocol gives
# us to do so is by sending an HTTP error back on the transaction.
#
# We log the exception now, and then raise a new SynapseError to cause
# the transaction to be failed.
logger.exception("Error handling EDU of type %s", edu.edu_type)
raise SynapseError(500, f"Error handing EDU of type {edu.edu_type}")
# TODO: if the first EDU fails, we should probably abort the whole
# thing rather than carrying on with the rest of them. That would
# probably be best done inside `concurrently_execute`.
await self.registry.on_edu(edu.edu_type, origin, edu.content)
await concurrently_execute(
_process_edu,
@@ -1432,7 +1414,12 @@ class FederationHandlerRegistry:
handler = self.edu_handlers.get(edu_type)
if handler:
with start_active_span_from_edu(content, "handle_edu"):
await handler(origin, content)
try:
await handler(origin, content)
except SynapseError as e:
logger.info("Failed to handle edu %r: %r", edu_type, e)
except Exception:
logger.exception("Failed to handle edu %r", edu_type)
return
# Check if we can route it somewhere else that isn't us
@@ -1441,12 +1428,17 @@ class FederationHandlerRegistry:
# Pick an instance randomly so that we don't overload one.
route_to = random.choice(instances)
await self._send_edu(
instance_name=route_to,
edu_type=edu_type,
origin=origin,
content=content,
)
try:
await self._send_edu(
instance_name=route_to,
edu_type=edu_type,
origin=origin,
content=content,
)
except SynapseError as e:
logger.info("Failed to handle edu %r: %r", edu_type, e)
except Exception:
logger.exception("Failed to handle edu %r", edu_type)
return
# Oh well, let's just log and move on.
+1 -5
View File
@@ -42,7 +42,6 @@ class AdminHandler:
self._device_handler = hs.get_device_handler()
self._storage_controllers = hs.get_storage_controllers()
self._state_storage_controller = self._storage_controllers.state
self._hs_config = hs.config
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
async def get_whois(self, user: UserID) -> JsonMapping:
@@ -218,10 +217,7 @@ class AdminHandler:
)
events = await filter_events_for_client(
self._storage_controllers,
user_id,
events,
msc4115_membership_on_events=self._hs_config.experimental.msc4115_membership_on_events,
self._storage_controllers, user_id, events
)
writer.write_events(room_id, events)
+1 -12
View File
@@ -261,22 +261,11 @@ class DeactivateAccountHandler:
user = UserID.from_string(user_id)
rooms_for_user = await self.store.get_rooms_for_user(user_id)
requester = create_requester(user, authenticated_entity=self._server_name)
should_erase = await self.store.is_user_erased(user_id)
for room_id in rooms_for_user:
logger.info("User parter parting %r from %r", user_id, room_id)
try:
# Before parting the user, redact all membership events if requested
if should_erase:
event_ids = await self.store.get_membership_event_ids_for_user(
user_id, room_id
)
for event_id in event_ids:
await self.store.expire_event(event_id)
await self._room_member_handler.update_membership(
requester,
create_requester(user, authenticated_entity=self._server_name),
user,
room_id,
"leave",
-3
View File
@@ -104,9 +104,6 @@ class DeviceMessageHandler:
"""
Handle receiving to-device messages from remote homeservers.
Note that any errors thrown from this method will cause the federation /send
request to receive an error response.
Args:
origin: The remote homeserver.
content: The JSON dictionary containing the to-device messages.
+1 -6
View File
@@ -148,7 +148,6 @@ class EventHandler:
def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastores().main
self._storage_controllers = hs.get_storage_controllers()
self._config = hs.config
async def get_event(
self,
@@ -190,11 +189,7 @@ class EventHandler:
is_peeking = not is_user_in_room
filtered = await filter_events_for_client(
self._storage_controllers,
user.to_string(),
[event],
is_peeking=is_peeking,
msc4115_membership_on_events=self._config.experimental.msc4115_membership_on_events,
self._storage_controllers, user.to_string(), [event], is_peeking=is_peeking
)
if not filtered:
+1 -6
View File
@@ -221,10 +221,7 @@ class InitialSyncHandler:
).addErrback(unwrapFirstError)
messages = await filter_events_for_client(
self._storage_controllers,
user_id,
messages,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
self._storage_controllers, user_id, messages
)
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
@@ -383,7 +380,6 @@ class InitialSyncHandler:
requester.user.to_string(),
messages,
is_peeking=is_peeking,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
)
start_token = StreamToken.START.copy_and_replace(StreamKeyType.ROOM, token)
@@ -498,7 +494,6 @@ class InitialSyncHandler:
requester.user.to_string(),
messages,
is_peeking=is_peeking,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
)
start_token = now_token.copy_and_replace(StreamKeyType.ROOM, token)
-1
View File
@@ -623,7 +623,6 @@ class PaginationHandler:
user_id,
events,
is_peeking=(member_event_id is None),
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
)
# if after the filter applied there are no more events
-3
View File
@@ -95,7 +95,6 @@ class RelationsHandler:
self._event_handler = hs.get_event_handler()
self._event_serializer = hs.get_event_client_serializer()
self._event_creation_handler = hs.get_event_creation_handler()
self._config = hs.config
async def get_relations(
self,
@@ -164,7 +163,6 @@ class RelationsHandler:
user_id,
events,
is_peeking=(member_event_id is None),
msc4115_membership_on_events=self._config.experimental.msc4115_membership_on_events,
)
# The relations returned for the requested event do include their
@@ -610,7 +608,6 @@ class RelationsHandler:
user_id,
events,
is_peeking=(member_event_id is None),
msc4115_membership_on_events=self._config.experimental.msc4115_membership_on_events,
)
aggregations = await self.get_bundled_aggregations(
-1
View File
@@ -1476,7 +1476,6 @@ class RoomContextHandler:
user.to_string(),
events,
is_peeking=is_peeking,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
)
event = await self.store.get_event(
-30
View File
@@ -752,36 +752,6 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
and requester.user.to_string() == self._server_notices_mxid
)
requester_suspended = await self.store.get_user_suspended_status(
requester.user.to_string()
)
if action == Membership.INVITE and requester_suspended:
raise SynapseError(
403,
"Sending invites while account is suspended is not allowed.",
Codes.USER_ACCOUNT_SUSPENDED,
)
if target.to_string() != requester.user.to_string():
target_suspended = await self.store.get_user_suspended_status(
target.to_string()
)
else:
target_suspended = requester_suspended
if action == Membership.JOIN and target_suspended:
raise SynapseError(
403,
"Joining rooms while account is suspended is not allowed.",
Codes.USER_ACCOUNT_SUSPENDED,
)
if action == Membership.KNOCK and target_suspended:
raise SynapseError(
403,
"Knocking on rooms while account is suspended is not allowed.",
Codes.USER_ACCOUNT_SUSPENDED,
)
if (
not self.allow_per_room_profiles and not is_requester_server_notices_user
) or requester.shadow_banned:
+4 -16
View File
@@ -480,10 +480,7 @@ class SearchHandler:
filtered_events = await search_filter.filter([r["event"] for r in results])
events = await filter_events_for_client(
self._storage_controllers,
user.to_string(),
filtered_events,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
self._storage_controllers, user.to_string(), filtered_events
)
events.sort(key=lambda e: -rank_map[e.event_id])
@@ -582,10 +579,7 @@ class SearchHandler:
filtered_events = await search_filter.filter([r["event"] for r in results])
events = await filter_events_for_client(
self._storage_controllers,
user.to_string(),
filtered_events,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
self._storage_controllers, user.to_string(), filtered_events
)
room_events.extend(events)
@@ -670,17 +664,11 @@ class SearchHandler:
)
events_before = await filter_events_for_client(
self._storage_controllers,
user.to_string(),
res.events_before,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
self._storage_controllers, user.to_string(), res.events_before
)
events_after = await filter_events_for_client(
self._storage_controllers,
user.to_string(),
res.events_after,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
self._storage_controllers, user.to_string(), res.events_after
)
context: JsonDict = {
-10
View File
@@ -169,7 +169,6 @@ class UsernameMappingSession:
# attributes returned by the ID mapper
display_name: Optional[str]
emails: StrCollection
avatar_url: Optional[str]
# An optional dictionary of extra attributes to be provided to the client in the
# login response.
@@ -184,7 +183,6 @@ class UsernameMappingSession:
# choices made by the user
chosen_localpart: Optional[str] = None
use_display_name: bool = True
use_avatar: bool = True
emails_to_use: StrCollection = ()
terms_accepted_version: Optional[str] = None
@@ -662,9 +660,6 @@ class SsoHandler:
remote_user_id=remote_user_id,
display_name=attributes.display_name,
emails=attributes.emails,
avatar_url=attributes.picture,
# Default to using all mapped emails. Will be overwritten in handle_submit_username_request.
emails_to_use=attributes.emails,
client_redirect_url=client_redirect_url,
expiry_time_ms=now + self._MAPPING_SESSION_VALIDITY_PERIOD_MS,
extra_login_attributes=extra_login_attributes,
@@ -971,7 +966,6 @@ class SsoHandler:
session_id: str,
localpart: str,
use_display_name: bool,
use_avatar: bool,
emails_to_use: Iterable[str],
) -> None:
"""Handle a request to the username-picker 'submit' endpoint
@@ -994,7 +988,6 @@ class SsoHandler:
# update the session with the user's choices
session.chosen_localpart = localpart
session.use_display_name = use_display_name
session.use_avatar = use_avatar
emails_from_idp = set(session.emails)
filtered_emails: Set[str] = set()
@@ -1075,9 +1068,6 @@ class SsoHandler:
if session.use_display_name:
attributes.display_name = session.display_name
if session.use_avatar:
attributes.picture = session.avatar_url
# the following will raise a 400 error if the username has been taken in the
# meantime.
user_id = await self._register_mapped_user(
-2
View File
@@ -596,7 +596,6 @@ class SyncHandler:
sync_config.user.to_string(),
recents,
always_include_ids=current_state_ids,
msc4115_membership_on_events=self.hs_config.experimental.msc4115_membership_on_events,
)
log_kv({"recents_after_visibility_filtering": len(recents)})
else:
@@ -682,7 +681,6 @@ class SyncHandler:
sync_config.user.to_string(),
loaded_recents,
always_include_ids=current_state_ids,
msc4115_membership_on_events=self.hs_config.experimental.msc4115_membership_on_events,
)
loaded_recents = []
+2 -3
View File
@@ -909,9 +909,8 @@ def set_cors_headers(request: "SynapseRequest") -> None:
request.setHeader(
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
)
if request.path is not None and (
request.path == b"/_matrix/client/unstable/org.matrix.msc4108/rendezvous"
or request.path.startswith(b"/_synapse/client/rendezvous")
if request.path is not None and request.path.startswith(
b"/_matrix/client/unstable/org.matrix.msc4108/rendezvous"
):
request.setHeader(
b"Access-Control-Allow-Headers",
-1
View File
@@ -721,7 +721,6 @@ class Notifier:
user.to_string(),
new_events,
is_peeking=is_peeking,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
)
elif keyname == StreamKeyType.PRESENCE:
now = self.clock.time_msec()
+1 -20
View File
@@ -205,22 +205,6 @@ class Mailer:
template_vars,
)
emails_sent_counter.labels("already_in_use")
async def send_already_in_use_mail(self, email_address: str) -> None:
"""Send an email if the address is already bound to an user account
Args:
email_address: Email address we're sending to the "already in use" mail
"""
await self.send_email(
email_address,
self.email_subjects.email_already_in_use
% {"server_name": self.hs.config.server.server_name, "app": self.app_name},
{},
)
emails_sent_counter.labels("add_threepid")
async def send_add_threepid_mail(
@@ -529,10 +513,7 @@ class Mailer:
}
the_events = await filter_events_for_client(
self._storage_controllers,
user_id,
results.events_before,
msc4115_membership_on_events=self.hs.config.experimental.msc4115_membership_on_events,
self._storage_controllers, user_id, results.events_before
)
the_events.append(notif_event)
-12
View File
@@ -1,12 +0,0 @@
{% extends "_base.html" %}
{% block title %}Email already in use{% endblock %}
{% block body %}
<p>You have asked us to register this email with a new Matrix account, but this email is already registered with an existing account.</p>
<p>Please reset your password if needed.</p>
<p>If this was not you, you can safely disregard this email.</p>
<p>Thank you.</p>
{% endblock %}
-10
View File
@@ -1,10 +0,0 @@
Hello there,
You have asked us to register this email with a new Matrix account,
but this email is already registered with an existing account.
Please reset your password if needed.
If this was not you, you can safely disregard this email.
Thank you.
+5 -8
View File
@@ -393,20 +393,17 @@ class SigningKeyUploadServlet(RestServlet):
# time. Because there is no UIA in MSC3861, for now we throw an error if the
# user tries to reset the device signing key when MSC3861 is enabled, but allow
# first-time setup.
#
# XXX: We now have a get-out clause by which MAS can temporarily mark the master
# key as replaceable. It should do its own equivalent of user interactive auth
# before doing so.
if self.hs.config.experimental.msc3861.enabled:
# The auth service has to explicitly mark the master key as replaceable
# without UIA to reset the device signing key with MSC3861.
if is_cross_signing_setup and not master_key_updatable_without_uia:
config = self.hs.config.experimental.msc3861
if config.account_management_url is not None:
url = f"{config.account_management_url}?action=org.matrix.cross_signing_reset"
else:
url = config.issuer
raise SynapseError(
HTTPStatus.NOT_IMPLEMENTED,
"To reset your end-to-end encryption cross-signing identity, "
f"you first need to approve it at {url} and then try again.",
"Resetting cross signing keys is not yet supported with MSC3861",
Codes.UNRECOGNIZED,
)
# But first-time setup is fine
+2 -10
View File
@@ -86,18 +86,12 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
self.config = hs.config
if self.hs.config.email.can_verify_email:
self.registration_mailer = Mailer(
self.mailer = Mailer(
hs=self.hs,
app_name=self.config.email.email_app_name,
template_html=self.config.email.email_registration_template_html,
template_text=self.config.email.email_registration_template_text,
)
self.already_in_use_mailer = Mailer(
hs=self.hs,
app_name=self.config.email.email_app_name,
template_html=self.config.email.email_already_in_use_template_html,
template_text=self.config.email.email_already_in_use_template_text,
)
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
if not self.hs.config.email.can_verify_email:
@@ -145,10 +139,8 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
if self.hs.config.server.request_token_inhibit_3pid_errors:
# Make the client think the operation succeeded. See the rationale in the
# comments for request_token_inhibit_3pid_errors.
# Still send an email to warn the user that an account already exists.
# Also wait for some random amount of time between 100ms and 1s to make it
# look like we did something.
await self.already_in_use_mailer.send_already_in_use_mail(email)
await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
return 200, {"sid": random_string(16)}
@@ -159,7 +151,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
email,
client_secret,
send_attempt,
self.registration_mailer.send_registration_mail,
self.mailer.send_registration_mail,
next_link,
)
-16
View File
@@ -97,25 +97,9 @@ class MSC4108DelegationRendezvousServlet(RestServlet):
)
class MSC4108RendezvousServlet(RestServlet):
PATTERNS = client_patterns(
"/org.matrix.msc4108/rendezvous$", releases=[], v1=False, unstable=True
)
def __init__(self, hs: "HomeServer") -> None:
super().__init__()
self._handler = hs.get_rendezvous_handler()
def on_POST(self, request: SynapseRequest) -> None:
self._handler.handle_post(request)
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.experimental.msc3886_endpoint is not None:
MSC3886RendezvousServlet(hs).register(http_server)
if hs.config.experimental.msc4108_enabled:
MSC4108RendezvousServlet(hs).register(http_server)
if hs.config.experimental.msc4108_delegation_endpoint is not None:
MSC4108DelegationRendezvousServlet(hs).register(http_server)
-6
View File
@@ -1442,16 +1442,10 @@ class RoomHierarchyRestServlet(RestServlet):
class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet):
PATTERNS = (
# deprecated endpoint, to be removed
re.compile(
"^/_matrix/client/unstable/im.nheko.summary"
"/rooms/(?P<room_identifier>[^/]*)/summary$"
),
# recommended endpoint
re.compile(
"^/_matrix/client/unstable/im.nheko.summary"
"/summary/(?P<room_identifier>[^/]*)$"
),
)
CATEGORY = "Client API requests"
+2 -8
View File
@@ -89,7 +89,6 @@ class VersionsRestServlet(RestServlet):
"v1.7",
"v1.8",
"v1.9",
"v1.10",
],
# as per MSC1497:
"unstable_features": {
@@ -142,13 +141,8 @@ class VersionsRestServlet(RestServlet):
# Allows clients to handle push for encrypted events.
"org.matrix.msc4028": self.config.experimental.msc4028_push_encrypted_events,
# MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code
"org.matrix.msc4108": (
self.config.experimental.msc4108_enabled
or (
self.config.experimental.msc4108_delegation_endpoint
is not None
)
),
"org.matrix.msc4108": self.config.experimental.msc4108_delegation_endpoint
is not None,
},
},
)
-4
View File
@@ -26,7 +26,6 @@ from twisted.web.resource import Resource
from synapse.rest.synapse.client.new_user_consent import NewUserConsentResource
from synapse.rest.synapse.client.pick_idp import PickIdpResource
from synapse.rest.synapse.client.pick_username import pick_username_resource
from synapse.rest.synapse.client.rendezvous import MSC4108RendezvousSessionResource
from synapse.rest.synapse.client.sso_register import SsoRegisterResource
from synapse.rest.synapse.client.unsubscribe import UnsubscribeResource
@@ -77,9 +76,6 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
# To be removed in Synapse v1.32.0.
resources["/_matrix/saml2"] = res
if hs.config.experimental.msc4108_enabled:
resources["/_synapse/client/rendezvous"] = MSC4108RendezvousSessionResource(hs)
return resources
+1 -3
View File
@@ -113,7 +113,6 @@ class AccountDetailsResource(DirectServeHtmlResource):
"display_name": session.display_name,
"emails": session.emails,
"localpart": localpart,
"avatar_url": session.avatar_url,
},
}
@@ -135,7 +134,6 @@ class AccountDetailsResource(DirectServeHtmlResource):
try:
localpart = parse_string(request, "username", required=True)
use_display_name = parse_boolean(request, "use_display_name", default=False)
use_avatar = parse_boolean(request, "use_avatar", default=False)
try:
emails_to_use: List[str] = [
@@ -149,5 +147,5 @@ class AccountDetailsResource(DirectServeHtmlResource):
return
await self._sso_handler.handle_submit_username_request(
request, session_id, localpart, use_display_name, use_avatar, emails_to_use
request, session_id, localpart, use_display_name, emails_to_use
)
-58
View File
@@ -1,58 +0,0 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it 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.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
#
import logging
from typing import TYPE_CHECKING, List
from synapse.api.errors import UnrecognizedRequestError
from synapse.http.server import DirectServeJsonResource
from synapse.http.site import SynapseRequest
if TYPE_CHECKING:
from synapse.server import HomeServer
logger = logging.getLogger(__name__)
class MSC4108RendezvousSessionResource(DirectServeJsonResource):
isLeaf = True
def __init__(self, hs: "HomeServer") -> None:
super().__init__()
self._handler = hs.get_rendezvous_handler()
async def _async_render_GET(self, request: SynapseRequest) -> None:
postpath: List[bytes] = request.postpath # type: ignore
if len(postpath) != 1:
raise UnrecognizedRequestError()
session_id = postpath[0].decode("ascii")
self._handler.handle_get(request, session_id)
def _async_render_PUT(self, request: SynapseRequest) -> None:
postpath: List[bytes] = request.postpath # type: ignore
if len(postpath) != 1:
raise UnrecognizedRequestError()
session_id = postpath[0].decode("ascii")
self._handler.handle_put(request, session_id)
def _async_render_DELETE(self, request: SynapseRequest) -> None:
postpath: List[bytes] = request.postpath # type: ignore
if len(postpath) != 1:
raise UnrecognizedRequestError()
session_id = postpath[0].decode("ascii")
self._handler.handle_delete(request, session_id)
-5
View File
@@ -143,7 +143,6 @@ from synapse.state import StateHandler, StateResolutionHandler
from synapse.storage import Databases
from synapse.storage.controllers import StorageControllers
from synapse.streams.events import EventSources
from synapse.synapse_rust.rendezvous import RendezvousHandler
from synapse.types import DomainSpecificString, ISynapseReactor
from synapse.util import Clock
from synapse.util.distributor import Distributor
@@ -860,10 +859,6 @@ class HomeServer(metaclass=abc.ABCMeta):
def get_room_forgetter_handler(self) -> RoomForgetterHandler:
return RoomForgetterHandler(self)
@cache_in_self
def get_rendezvous_handler(self) -> RendezvousHandler:
return RendezvousHandler(self)
@cache_in_self
def get_outbound_redis_connection(self) -> "ConnectionHandler":
"""
+71 -37
View File
@@ -19,7 +19,6 @@
# [This file includes modifications made by New Vector Limited]
#
#
import collections
import itertools
import logging
from collections import OrderedDict
@@ -54,7 +53,6 @@ from synapse.storage.database import (
LoggingDatabaseConnection,
LoggingTransaction,
)
from synapse.storage.databases.main.event_federation import EventFederationStore
from synapse.storage.databases.main.events_worker import EventCacheEntry
from synapse.storage.databases.main.search import SearchEntry
from synapse.storage.engines import PostgresEngine
@@ -770,26 +768,40 @@ class PersistEventsStore:
# that have the same chain ID as the event.
# 2. For each retained auth event we:
# a. Add a link from the event's to the auth event's chain
# ID/sequence number
# ID/sequence number; and
# b. Add a link from the event to every chain reachable by the
# auth event.
# Step 1, fetch all existing links from all the chains we've seen
# referenced.
chain_links = _LinkMap()
for links in EventFederationStore._get_chain_links(
txn, {chain_id for chain_id, _ in chain_map.values()}
):
for origin_chain_id, inner_links in links.items():
for (
origin_sequence_number,
target_chain_id,
target_sequence_number,
) in inner_links:
chain_links.add_link(
(origin_chain_id, origin_sequence_number),
(target_chain_id, target_sequence_number),
new=False,
)
auth_chain_rows = cast(
List[Tuple[int, int, int, int]],
db_pool.simple_select_many_txn(
txn,
table="event_auth_chain_links",
column="origin_chain_id",
iterable={chain_id for chain_id, _ in chain_map.values()},
keyvalues={},
retcols=(
"origin_chain_id",
"origin_sequence_number",
"target_chain_id",
"target_sequence_number",
),
),
)
for (
origin_chain_id,
origin_sequence_number,
target_chain_id,
target_sequence_number,
) in auth_chain_rows:
chain_links.add_link(
(origin_chain_id, origin_sequence_number),
(target_chain_id, target_sequence_number),
new=False,
)
# We do this in toplogical order to avoid adding redundant links.
for event_id in sorted_topologically(
@@ -824,6 +836,18 @@ class PersistEventsStore:
(chain_id, sequence_number), (auth_chain_id, auth_sequence_number)
)
# Step 2b, add a link to chains reachable from the auth
# event.
for target_id, target_seq in chain_links.get_links_from(
(auth_chain_id, auth_sequence_number)
):
if target_id == chain_id:
continue
chain_links.add_link(
(chain_id, sequence_number), (target_id, target_seq)
)
db_pool.simple_insert_many_txn(
txn,
table="event_auth_chain_links",
@@ -2427,6 +2451,31 @@ class _LinkMap:
current_links[src_seq] = target_seq
return True
def get_links_from(
self, src_tuple: Tuple[int, int]
) -> Generator[Tuple[int, int], None, None]:
"""Gets the chains reachable from the given chain/sequence number.
Yields:
The chain ID and sequence number the link points to.
"""
src_chain, src_seq = src_tuple
for target_id, sequence_numbers in self.maps.get(src_chain, {}).items():
for link_src_seq, target_seq in sequence_numbers.items():
if link_src_seq <= src_seq:
yield target_id, target_seq
def get_links_between(
self, source_chain: int, target_chain: int
) -> Generator[Tuple[int, int], None, None]:
"""Gets the links between two chains.
Yields:
The source and target sequence numbers.
"""
yield from self.maps.get(source_chain, {}).get(target_chain, {}).items()
def get_additions(self) -> Generator[Tuple[int, int, int, int], None, None]:
"""Gets any newly added links.
@@ -2453,24 +2502,9 @@ class _LinkMap:
if src_chain == target_chain:
return target_seq <= src_seq
# We have to graph traverse the links to check for indirect paths.
visited_chains: Dict[int, int] = collections.Counter()
search = [(src_chain, src_seq)]
while search:
chain, seq = search.pop()
visited_chains[chain] = max(seq, visited_chains[chain])
for tc, links in self.maps.get(chain, {}).items():
for ss, ts in links.items():
# Don't revisit chains we've already seen, unless the target
# sequence number is higher than last time.
if ts <= visited_chains.get(tc, 0):
continue
if ss <= seq:
if tc == target_chain:
if target_seq <= ts:
return True
else:
search.append((tc, ts))
links = self.get_links_between(src_chain, target_chain)
for link_start_seq, link_end_seq in links:
if link_start_seq <= src_seq and target_seq <= link_end_seq:
return True
return False
+1 -54
View File
@@ -236,8 +236,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
consent_server_notice_sent, appservice_id, creation_ts, user_type,
deactivated, COALESCE(shadow_banned, FALSE) AS shadow_banned,
COALESCE(approved, TRUE) AS approved,
COALESCE(locked, FALSE) AS locked,
suspended
COALESCE(locked, FALSE) AS locked
FROM users
WHERE name = ?
""",
@@ -262,7 +261,6 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
shadow_banned,
approved,
locked,
suspended,
) = row
return UserInfo(
@@ -279,7 +277,6 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
user_type=user_type,
approved=bool(approved),
locked=bool(locked),
suspended=bool(suspended),
)
return await self.db_pool.runInteraction(
@@ -1183,27 +1180,6 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
# Convert the potential integer into a boolean.
return bool(res)
@cached()
async def get_user_suspended_status(self, user_id: str) -> bool:
"""
Determine whether the user's account is suspended.
Args:
user_id: The user ID of the user in question
Returns:
True if the user's account is suspended, false if it is not suspended or
if the user ID cannot be found.
"""
res = await self.db_pool.simple_select_one_onecol(
table="users",
keyvalues={"name": user_id},
retcol="suspended",
allow_none=True,
desc="get_user_suspended",
)
return bool(res)
async def get_threepid_validation_session(
self,
medium: Optional[str],
@@ -2237,35 +2213,6 @@ class RegistrationBackgroundUpdateStore(RegistrationWorkerStore):
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
txn.call_after(self.is_guest.invalidate, (user_id,))
async def set_user_suspended_status(self, user_id: str, suspended: bool) -> None:
"""
Set whether the user's account is suspended in the `users` table.
Args:
user_id: The user ID of the user in question
suspended: True if the user is suspended, false if not
"""
await self.db_pool.runInteraction(
"set_user_suspended_status",
self.set_user_suspended_status_txn,
user_id,
suspended,
)
def set_user_suspended_status_txn(
self, txn: LoggingTransaction, user_id: str, suspended: bool
) -> None:
self.db_pool.simple_update_one_txn(
txn=txn,
table="users",
keyvalues={"name": user_id},
updatevalues={"suspended": suspended},
)
self._invalidate_cache_and_stream(
txn, self.get_user_suspended_status, (user_id,)
)
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
async def set_user_locked_status(self, user_id: str, locked: bool) -> None:
"""Set the `locked` property for the provided user to the provided value.
@@ -1234,28 +1234,6 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
return set(room_ids)
async def get_membership_event_ids_for_user(
self, user_id: str, room_id: str
) -> Set[str]:
"""Get all event_ids for the given user and room.
Args:
user_id: The user ID to get the event IDs for.
room_id: The room ID to look up events for.
Returns:
Set of event IDs
"""
event_ids = await self.db_pool.simple_select_onecol(
table="room_memberships",
keyvalues={"user_id": user_id, "room_id": room_id},
retcol="event_id",
desc="get_membership_event_ids_for_user",
)
return set(event_ids)
@cached(max_entries=5000)
async def _get_membership_from_event_id(
self, member_event_id: str
+7 -24
View File
@@ -470,8 +470,6 @@ class SearchStore(SearchBackgroundUpdateStore):
count_args = args
count_clauses = clauses
sqlite_highlights: List[str] = []
if isinstance(self.database_engine, PostgresEngine):
search_query = search_term
sql = """
@@ -488,7 +486,7 @@ class SearchStore(SearchBackgroundUpdateStore):
"""
count_args = [search_query] + count_args
elif isinstance(self.database_engine, Sqlite3Engine):
search_query, sqlite_highlights = _parse_query_for_sqlite(search_term)
search_query = _parse_query_for_sqlite(search_term)
sql = """
SELECT rank(matchinfo(event_search)) as rank, room_id, event_id
@@ -533,11 +531,9 @@ class SearchStore(SearchBackgroundUpdateStore):
event_map = {ev.event_id: ev for ev in events}
highlights: Collection[str] = []
highlights = None
if isinstance(self.database_engine, PostgresEngine):
highlights = await self._find_highlights_in_postgres(search_query, events)
else:
highlights = sqlite_highlights
count_sql += " GROUP BY room_id"
@@ -601,8 +597,6 @@ class SearchStore(SearchBackgroundUpdateStore):
count_args = list(args)
count_clauses = list(clauses)
sqlite_highlights: List[str] = []
if pagination_token:
try:
origin_server_ts_str, stream_str = pagination_token.split(",")
@@ -653,7 +647,7 @@ class SearchStore(SearchBackgroundUpdateStore):
CROSS JOIN events USING (event_id)
WHERE
"""
search_query, sqlite_highlights = _parse_query_for_sqlite(search_term)
search_query = _parse_query_for_sqlite(search_term)
args = [search_query] + args
count_sql = """
@@ -700,11 +694,9 @@ class SearchStore(SearchBackgroundUpdateStore):
event_map = {ev.event_id: ev for ev in events}
highlights: Collection[str] = []
highlights = None
if isinstance(self.database_engine, PostgresEngine):
highlights = await self._find_highlights_in_postgres(search_query, events)
else:
highlights = sqlite_highlights
count_sql += " GROUP BY room_id"
@@ -900,25 +892,19 @@ def _tokenize_query(query: str) -> TokenList:
return tokens
def _tokens_to_sqlite_match_query(tokens: TokenList) -> Tuple[str, List[str]]:
def _tokens_to_sqlite_match_query(tokens: TokenList) -> str:
"""
Convert the list of tokens to a string suitable for passing to sqlite's MATCH.
Assume sqlite was compiled with enhanced query syntax.
Returns the sqlite-formatted query string and the tokenized search terms
that can be used as highlights.
Ref: https://www.sqlite.org/fts3.html#full_text_index_queries
"""
match_query = []
highlights = []
for token in tokens:
if isinstance(token, str):
match_query.append(token)
highlights.append(token)
elif isinstance(token, Phrase):
match_query.append('"' + " ".join(token.phrase) + '"')
highlights.append(" ".join(token.phrase))
elif token == SearchToken.Not:
# TODO: SQLite treats NOT as a *binary* operator. Hopefully a search
# term has already been added before this.
@@ -930,14 +916,11 @@ def _tokens_to_sqlite_match_query(tokens: TokenList) -> Tuple[str, List[str]]:
else:
raise ValueError(f"unknown token {token}")
return "".join(match_query), highlights
return "".join(match_query)
def _parse_query_for_sqlite(search_term: str) -> Tuple[str, List[str]]:
def _parse_query_for_sqlite(search_term: str) -> str:
"""Takes a plain unicode string from the user and converts it into a form
that can be passed to sqllite's matchinfo().
Returns the converted query string and the tokenized search terms
that can be used as highlights.
"""
return _tokens_to_sqlite_match_query(_tokenize_query(search_term))
@@ -660,7 +660,6 @@ class TransactionWorkerStore(CacheInvalidationWorkerStore):
limit=limit,
retcols=("room_id", "stream_ordering"),
order_direction=order,
keyvalues={"destination": destination},
),
)
return rooms, count
+3 -10
View File
@@ -19,7 +19,7 @@
#
#
SCHEMA_VERSION = 85 # remember to update the list below when updating
SCHEMA_VERSION = 84 # remember to update the list below when updating
"""Represents the expectations made by the codebase about the database schema
This should be incremented whenever the codebase changes its requirements on the
@@ -132,19 +132,12 @@ Changes in SCHEMA_VERSION = 82
Changes in SCHEMA_VERSION = 83
- The event_txn_id is no longer used.
Changes in SCHEMA_VERSION = 84
- No longer assumes that `event_auth_chain_links` holds transitive links, and
so read operations must do graph traversal.
Changes in SCHEMA_VERSION = 85
- Add a column `suspended` to the `users` table
"""
SCHEMA_COMPAT_VERSION = (
# Transitive links are no longer written to `event_auth_chain_links`
84
# The event_txn_id table and tables from MSC2716 no longer exist.
83
)
"""Limit on how far the synapse codebase can be rolled back without breaking db compat
@@ -1,14 +0,0 @@
--
-- This file is licensed under the Affero General Public License (AGPL) version 3.
--
-- Copyright (C) 2024 New Vector, Ltd
--
-- This program is free software: you can redistribute it and/or modify
-- it 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.
--
-- See the GNU Affero General Public License for more details:
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
ALTER TABLE users ADD COLUMN suspended BOOLEAN DEFAULT FALSE NOT NULL;
-30
View File
@@ -1,30 +0,0 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it 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.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
from twisted.web.iweb import IRequest
from synapse.server import HomeServer
class RendezvousHandler:
def __init__(
self,
homeserver: HomeServer,
/,
capacity: int = 100,
max_content_length: int = 4 * 1024, # MSC4108 specifies 4KB
eviction_interval: int = 60 * 1000,
ttl: int = 60 * 1000,
) -> None: ...
def handle_post(self, request: IRequest) -> None: ...
def handle_get(self, request: IRequest, session_id: str) -> None: ...
def handle_put(self, request: IRequest, session_id: str) -> None: ...
def handle_delete(self, request: IRequest, session_id: str) -> None: ...
-2
View File
@@ -1156,7 +1156,6 @@ class UserInfo:
user_type: User type (None for normal user, 'support' and 'bot' other options).
approved: If the user has been "approved" to register on the server.
locked: Whether the user's account has been locked
suspended: Whether the user's account is currently suspended
"""
user_id: UserID
@@ -1172,7 +1171,6 @@ class UserInfo:
is_shadow_banned: bool
approved: bool
locked: bool
suspended: bool
class UserProfile(TypedDict):
+2 -20
View File
@@ -115,7 +115,7 @@ class StreamChangeCache:
"""
new_size = math.floor(self._original_max_size * factor)
if new_size != self._max_size:
self._max_size = new_size
self.max_size = new_size
self._evict()
return True
return False
@@ -165,7 +165,7 @@ class StreamChangeCache:
return False
def get_entities_changed(
self, entities: Collection[EntityType], stream_pos: int, _perf_factor: int = 1
self, entities: Collection[EntityType], stream_pos: int
) -> Union[Set[EntityType], FrozenSet[EntityType]]:
"""
Returns the subset of the given entities that have had changes after the given position.
@@ -177,8 +177,6 @@ class StreamChangeCache:
Args:
entities: Entities to check for changes.
stream_pos: The stream position to check for changes after.
_perf_factor: Used by unit tests to choose when to use each
optimisation.
Return:
A subset of entities which have changed after the given stream position.
@@ -186,22 +184,6 @@ class StreamChangeCache:
This will be all entities if the given stream position is at or earlier
than the earliest known stream position.
"""
if not self._cache or stream_pos <= self._earliest_known_stream_pos:
self.metrics.inc_misses()
return set(entities)
# If there have been tonnes of changes compared with the number of
# entities, it is faster to check each entities stream ordering
# one-by-one.
max_stream_pos, _ = self._cache.peekitem()
if max_stream_pos - stream_pos > _perf_factor * len(entities):
self.metrics.inc_hits()
return {
entity
for entity in entities
if self._entity_to_key.get(entity, -1) > stream_pos
}
cache_result = self.get_all_entities_changed(stream_pos)
if cache_result.hit:
# We now do an intersection, trying to do so in the most efficient
+15 -58
View File
@@ -36,15 +36,10 @@ from typing import (
import attr
from synapse.api.constants import (
EventTypes,
EventUnsignedContentFields,
HistoryVisibility,
Membership,
)
from synapse.api.constants import EventTypes, HistoryVisibility, Membership
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.events.utils import clone_event, prune_event
from synapse.events.utils import prune_event
from synapse.logging.opentracing import trace
from synapse.storage.controllers import StorageControllers
from synapse.storage.databases.main import DataStore
@@ -82,7 +77,6 @@ async def filter_events_for_client(
is_peeking: bool = False,
always_include_ids: FrozenSet[str] = frozenset(),
filter_send_to_client: bool = True,
msc4115_membership_on_events: bool = False,
) -> List[EventBase]:
"""
Check which events a user is allowed to see. If the user can see the event but its
@@ -101,12 +95,9 @@ async def filter_events_for_client(
filter_send_to_client: Whether we're checking an event that's going to be
sent to a client. This might not always be the case since this function can
also be called to check whether a user can see the state at a given point.
msc4115_membership_on_events: Whether to include the requesting user's
membership in the "unsigned" data, per MSC4115.
Returns:
The filtered events. If `msc4115_membership_on_events` is true, the `unsigned`
data is annotated with the membership state of `user_id` at each event.
The filtered events.
"""
# Filter out events that have been soft failed so that we don't relay them
# to clients.
@@ -143,8 +134,7 @@ async def filter_events_for_client(
)
def allowed(event: EventBase) -> Optional[EventBase]:
state_after_event = event_id_to_state.get(event.event_id)
filtered = _check_client_allowed_to_see_event(
return _check_client_allowed_to_see_event(
user_id=user_id,
event=event,
clock=storage.main.clock,
@@ -152,45 +142,13 @@ async def filter_events_for_client(
sender_ignored=event.sender in ignore_list,
always_include_ids=always_include_ids,
retention_policy=retention_policies[room_id],
state=state_after_event,
state=event_id_to_state.get(event.event_id),
is_peeking=is_peeking,
sender_erased=erased_senders.get(event.sender, False),
)
if filtered is None:
return None
if not msc4115_membership_on_events:
return filtered
# Annotate the event with the user's membership after the event.
#
# Normally we just look in `state_after_event`, but if the event is an outlier
# we won't have such a state. The only outliers that are returned here are the
# user's own membership event, so we can just inspect that.
user_membership_event: Optional[EventBase]
if event.type == EventTypes.Member and event.state_key == user_id:
user_membership_event = event
elif state_after_event is not None:
user_membership_event = state_after_event.get((EventTypes.Member, user_id))
else:
# unreachable!
raise Exception("Missing state for event that is not user's own membership")
user_membership = (
user_membership_event.membership
if user_membership_event
else Membership.LEAVE
)
# Copy the event before updating the unsigned data: this shouldn't be persisted
# to the cache!
cloned = clone_event(filtered)
cloned.unsigned[EventUnsignedContentFields.MSC4115_MEMBERSHIP] = user_membership
return cloned
# Check each event: gives an iterable of None or (a modified) EventBase.
# Check each event: gives an iterable of None or (a potentially modified)
# EventBase.
filtered_events = map(allowed, events)
# Turn it into a list and remove None entries before returning.
@@ -438,13 +396,7 @@ def _check_client_allowed_to_see_event(
@attr.s(frozen=True, slots=True, auto_attribs=True)
class _CheckMembershipReturn:
"""Return value of `_check_membership`.
Attributes:
allowed: Whether the user should be allowed to see the event.
joined: Whether the user was joined to the room at the event.
"""
"Return value of _check_membership"
allowed: bool
joined: bool
@@ -456,7 +408,12 @@ def _check_membership(
state: StateMap[EventBase],
is_peeking: bool,
) -> _CheckMembershipReturn:
"""Check whether the user can see the event due to their membership"""
"""Check whether the user can see the event due to their membership
Returns:
True if they can, False if they can't, plus the membership of the user
at the event.
"""
# If the event is the user's own membership event, use the 'most joined'
# membership
membership = None
@@ -478,7 +435,7 @@ def _check_membership(
if membership == "leave" and (
prev_membership == "join" or prev_membership == "invite"
):
return _CheckMembershipReturn(True, False)
return _CheckMembershipReturn(True, membership == Membership.JOIN)
new_priority = MEMBERSHIP_PRIORITY.index(membership)
old_priority = MEMBERSHIP_PRIORITY.index(prev_membership)
-24
View File
@@ -32,7 +32,6 @@ from synapse.events.utils import (
PowerLevelsContent,
SerializeEventConfig,
_split_field,
clone_event,
copy_and_fixup_power_levels_contents,
maybe_upsert_event_field,
prune_event,
@@ -612,29 +611,6 @@ class PruneEventTestCase(stdlib_unittest.TestCase):
)
class CloneEventTestCase(stdlib_unittest.TestCase):
def test_unsigned_is_copied(self) -> None:
original = make_event_from_dict(
{
"type": "A",
"event_id": "$test:domain",
"unsigned": {"a": 1, "b": 2},
},
RoomVersions.V1,
{"txn_id": "txn"},
)
original.internal_metadata.stream_ordering = 1234
self.assertEqual(original.internal_metadata.stream_ordering, 1234)
cloned = clone_event(original)
cloned.unsigned["b"] = 3
self.assertEqual(original.unsigned, {"a": 1, "b": 2})
self.assertEqual(cloned.unsigned, {"a": 1, "b": 3})
self.assertEqual(cloned.internal_metadata.stream_ordering, 1234)
self.assertEqual(cloned.internal_metadata.txn_id, "txn")
class SerializeEventTestCase(stdlib_unittest.TestCase):
def serialize(self, ev: EventBase, fields: Optional[List[str]]) -> JsonDict:
return serialize_event(
@@ -67,23 +67,6 @@ class FederationServerTests(unittest.FederatingHomeserverTestCase):
self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, channel.result)
self.assertEqual(channel.json_body["errcode"], "M_NOT_JSON")
def test_failed_edu_causes_500(self) -> None:
"""If the EDU handler fails, /send should return a 500."""
async def failing_handler(_origin: str, _content: JsonDict) -> None:
raise Exception("bleh")
self.hs.get_federation_registry().register_edu_handler(
"FAIL_EDU_TYPE", failing_handler
)
channel = self.make_signed_federation_request(
"PUT",
"/_matrix/federation/v1/send/txn",
{"edus": [{"edu_type": "FAIL_EDU_TYPE", "content": {}}]},
)
self.assertEqual(500, channel.code, channel.result)
class ServerACLsTestCase(unittest.TestCase):
def test_blocked_server(self) -> None:
+1 -8
View File
@@ -59,14 +59,7 @@ class RoomDirectoryFederationTests(unittest.FederatingHomeserverTestCase):
"/_matrix/federation/v1/send/txn_id_1234/",
content={
"edus": [
{
"edu_type": EduTypes.DEVICE_LIST_UPDATE,
"content": {
"device_id": "QBUAZIFURK",
"stream_id": 0,
"user_id": "@user:id",
},
},
{"edu_type": EduTypes.DEVICE_LIST_UPDATE, "content": {"foo": "bar"}}
],
"pdus": [],
},
-37
View File
@@ -424,40 +424,3 @@ class DeactivateAccountTestCase(HomeserverTestCase):
self._store.get_knocked_at_rooms_for_local_user(self.user)
)
self.assertEqual(len(after_deactivate_knocks), 0)
def test_membership_is_redacted_upon_deactivation(self) -> None:
"""
Tests that room membership events are redacted if erasure is requested.
"""
# Create a room
room_id = self.helper.create_room_as(
self.user,
is_public=True,
tok=self.token,
)
# Change the displayname
membership_event, _ = self.get_success(
self.handler.update_membership(
requester=create_requester(self.user),
target=UserID.from_string(self.user),
room_id=room_id,
action=Membership.JOIN,
content={"displayname": "Hello World!"},
)
)
# Deactivate the account
self._deactivate_my_account()
# Get the all membership event IDs
membership_event_ids = self.get_success(
self._store.get_membership_event_ids_for_user(self.user, room_id=room_id)
)
# Get the events incl. JSON
events = self.get_success(self._store.get_events_as_list(membership_event_ids))
# Validate that there is no displayname in any of the events
for event in events:
self.assertTrue("displayname" not in event.content)
+3 -64
View File
@@ -778,81 +778,20 @@ class DestinationMembershipTestCase(unittest.HomeserverTestCase):
self.assertEqual(number_rooms, len(channel.json_body["rooms"]))
self._check_fields(channel.json_body["rooms"])
def test_room_filtering(self) -> None:
"""Tests that rooms are correctly filtered"""
# Create two rooms on the homeserver. Each has a different remote homeserver
# participating in it.
other_destination = "other.destination.org"
room_ids_self_dest = self._create_destination_rooms(2, destination=self.dest)
room_ids_other_dest = self._create_destination_rooms(
1, destination=other_destination
)
# Ask for the rooms that `self.dest` is participating in.
channel = self.make_request("GET", self.url, access_token=self.admin_user_tok)
self.assertEqual(200, channel.code, msg=channel.json_body)
# Verify that we received only the rooms that `self.dest` is participating in.
# This assertion method name is a bit misleading. It does check that both lists
# contain the same items, and the same counts.
self.assertCountEqual(
[r["room_id"] for r in channel.json_body["rooms"]], room_ids_self_dest
)
self.assertEqual(channel.json_body["total"], len(room_ids_self_dest))
# Ask for the rooms that `other_destination` is participating in.
channel = self.make_request(
"GET",
self.url.replace(self.dest, other_destination),
access_token=self.admin_user_tok,
)
self.assertEqual(200, channel.code, msg=channel.json_body)
# Verify that we received only the rooms that `other_destination` is
# participating in.
self.assertCountEqual(
[r["room_id"] for r in channel.json_body["rooms"]], room_ids_other_dest
)
self.assertEqual(channel.json_body["total"], len(room_ids_other_dest))
def _create_destination_rooms(
self,
number_rooms: int,
destination: Optional[str] = None,
) -> List[str]:
"""
Create the given number of rooms. The given `destination` homeserver will
be recorded as a participant.
def _create_destination_rooms(self, number_rooms: int) -> None:
"""Create a number rooms for destination
Args:
number_rooms: Number of rooms to be created
destination: The domain of the homeserver that will be considered
as a participant in the rooms.
Returns:
The IDs of the rooms that have been created.
"""
room_ids = []
# If no destination was provided, default to `self.dest`.
if destination is None:
destination = self.dest
for _ in range(number_rooms):
room_id = self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok
)
room_ids.append(room_id)
self.get_success(
self.store.store_destination_rooms_entries(
(destination,), room_id, 1234
)
self.store.store_destination_rooms_entries((self.dest,), room_id, 1234)
)
return room_ids
def _check_fields(self, content: List[JsonDict]) -> None:
"""Checks that the expected room attributes are present in content
+14 -190
View File
@@ -20,17 +20,7 @@
#
import time
import urllib.parse
from typing import (
Any,
BinaryIO,
Callable,
Collection,
Dict,
List,
Optional,
Tuple,
Union,
)
from typing import Any, Collection, Dict, List, Optional, Tuple, Union
from unittest.mock import Mock
from urllib.parse import urlencode
@@ -44,9 +34,8 @@ import synapse.rest.admin
from synapse.api.constants import ApprovalNoticeMedium, LoginType
from synapse.api.errors import Codes
from synapse.appservice import ApplicationService
from synapse.http.client import RawHeaders
from synapse.module_api import ModuleApi
from synapse.rest.client import account, devices, login, logout, profile, register
from synapse.rest.client import devices, login, logout, register
from synapse.rest.client.account import WhoamiRestServlet
from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.server import HomeServer
@@ -59,7 +48,6 @@ from tests.handlers.test_saml import has_saml2
from tests.rest.client.utils import TEST_OIDC_CONFIG
from tests.server import FakeChannel
from tests.test_utils.html_parsers import TestHtmlParser
from tests.test_utils.oidc import FakeOidcServer
from tests.unittest import HomeserverTestCase, override_config, skip_unless
try:
@@ -1433,19 +1421,7 @@ class AppserviceLoginRestServletTestCase(unittest.HomeserverTestCase):
class UsernamePickerTestCase(HomeserverTestCase):
"""Tests for the username picker flow of SSO login"""
servlets = [
login.register_servlets,
profile.register_servlets,
account.register_servlets,
]
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
self.http_client = Mock(spec=["get_file"])
self.http_client.get_file.side_effect = mock_get_file
hs = self.setup_test_homeserver(
proxied_blocklisted_http_client=self.http_client
)
return hs
servlets = [login.register_servlets]
def default_config(self) -> Dict[str, Any]:
config = super().default_config()
@@ -1454,11 +1430,7 @@ class UsernamePickerTestCase(HomeserverTestCase):
config["oidc_config"] = {}
config["oidc_config"].update(TEST_OIDC_CONFIG)
config["oidc_config"]["user_mapping_provider"] = {
"config": {
"display_name_template": "{{ user.displayname }}",
"email_template": "{{ user.email }}",
"picture_template": "{{ user.picture }}",
}
"config": {"display_name_template": "{{ user.displayname }}"}
}
# whitelist this client URI so we redirect straight to it rather than
@@ -1471,22 +1443,15 @@ class UsernamePickerTestCase(HomeserverTestCase):
d.update(build_synapse_client_resource_tree(self.hs))
return d
def proceed_to_username_picker_page(
self,
fake_oidc_server: FakeOidcServer,
displayname: str,
email: str,
picture: str,
) -> Tuple[str, str]:
def test_username_picker(self) -> None:
"""Test the happy path of a username picker flow."""
fake_oidc_server = self.helper.fake_oidc_server()
# do the start of the login flow
channel, _ = self.helper.auth_via_oidc(
fake_oidc_server,
{
"sub": "tester",
"displayname": displayname,
"picture": picture,
"email": email,
},
{"sub": "tester", "displayname": "Jonny"},
TEST_CLIENT_REDIRECT_URL,
)
@@ -1513,42 +1478,16 @@ class UsernamePickerTestCase(HomeserverTestCase):
)
session = username_mapping_sessions[session_id]
self.assertEqual(session.remote_user_id, "tester")
self.assertEqual(session.display_name, displayname)
self.assertEqual(session.emails, [email])
self.assertEqual(session.avatar_url, picture)
self.assertEqual(session.display_name, "Jonny")
self.assertEqual(session.client_redirect_url, TEST_CLIENT_REDIRECT_URL)
# the expiry time should be about 15 minutes away
expected_expiry = self.clock.time_msec() + (15 * 60 * 1000)
self.assertApproximates(session.expiry_time_ms, expected_expiry, tolerance=1000)
return picker_url, session_id
def test_username_picker_use_displayname_avatar_and_email(self) -> None:
"""Test the happy path of a username picker flow with using displayname, avatar and email."""
fake_oidc_server = self.helper.fake_oidc_server()
mxid = "@bobby:test"
displayname = "Jonny"
email = "bobby@test.com"
picture = "mxc://test/avatar_url"
picker_url, session_id = self.proceed_to_username_picker_page(
fake_oidc_server, displayname, email, picture
)
# Now, submit a username to the username picker, which should serve a redirect
# to the completion page.
# Also specify that we should use the provided displayname, avatar and email.
content = urlencode(
{
b"username": b"bobby",
b"use_display_name": b"true",
b"use_avatar": b"true",
b"use_email": email,
}
).encode("utf8")
# to the completion page
content = urlencode({b"username": b"bobby"}).encode("utf8")
chan = self.make_request(
"POST",
path=picker_url,
@@ -1597,119 +1536,4 @@ class UsernamePickerTestCase(HomeserverTestCase):
content={"type": "m.login.token", "token": login_token},
)
self.assertEqual(chan.code, 200, chan.result)
self.assertEqual(chan.json_body["user_id"], mxid)
# ensure the displayname and avatar from the OIDC response have been configured for the user.
channel = self.make_request(
"GET", "/profile/" + mxid, access_token=chan.json_body["access_token"]
)
self.assertEqual(channel.code, 200, channel.result)
self.assertIn("mxc://test", channel.json_body["avatar_url"])
self.assertEqual(displayname, channel.json_body["displayname"])
# ensure the email from the OIDC response has been configured for the user.
channel = self.make_request(
"GET", "/account/3pid", access_token=chan.json_body["access_token"]
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(email, channel.json_body["threepids"][0]["address"])
def test_username_picker_dont_use_displayname_avatar_or_email(self) -> None:
"""Test the happy path of a username picker flow without using displayname, avatar or email."""
fake_oidc_server = self.helper.fake_oidc_server()
mxid = "@bobby:test"
displayname = "Jonny"
email = "bobby@test.com"
picture = "mxc://test/avatar_url"
username = "bobby"
picker_url, session_id = self.proceed_to_username_picker_page(
fake_oidc_server, displayname, email, picture
)
# Now, submit a username to the username picker, which should serve a redirect
# to the completion page.
# Also specify that we should not use the provided displayname, avatar or email.
content = urlencode(
{
b"username": username,
b"use_display_name": b"false",
b"use_avatar": b"false",
}
).encode("utf8")
chan = self.make_request(
"POST",
path=picker_url,
content=content,
content_is_form=True,
custom_headers=[
("Cookie", "username_mapping_session=" + session_id),
# old versions of twisted don't do form-parsing without a valid
# content-length header.
("Content-Length", str(len(content))),
],
)
self.assertEqual(chan.code, 302, chan.result)
location_headers = chan.headers.getRawHeaders("Location")
assert location_headers
# send a request to the completion page, which should 302 to the client redirectUrl
chan = self.make_request(
"GET",
path=location_headers[0],
custom_headers=[("Cookie", "username_mapping_session=" + session_id)],
)
self.assertEqual(chan.code, 302, chan.result)
location_headers = chan.headers.getRawHeaders("Location")
assert location_headers
# ensure that the returned location matches the requested redirect URL
path, query = location_headers[0].split("?", 1)
self.assertEqual(path, "https://x")
# it will have url-encoded the params properly, so we'll have to parse them
params = urllib.parse.parse_qsl(
query, keep_blank_values=True, strict_parsing=True, errors="strict"
)
self.assertEqual(params[0:2], EXPECTED_CLIENT_REDIRECT_URL_PARAMS)
self.assertEqual(params[2][0], "loginToken")
# fish the login token out of the returned redirect uri
login_token = params[2][1]
# finally, submit the matrix login token to the login API, which gives us our
# matrix access token, mxid, and device id.
chan = self.make_request(
"POST",
"/login",
content={"type": "m.login.token", "token": login_token},
)
self.assertEqual(chan.code, 200, chan.result)
self.assertEqual(chan.json_body["user_id"], mxid)
# ensure the displayname and avatar from the OIDC response have not been configured for the user.
channel = self.make_request(
"GET", "/profile/" + mxid, access_token=chan.json_body["access_token"]
)
self.assertEqual(channel.code, 200, channel.result)
self.assertNotIn("avatar_url", channel.json_body)
self.assertEqual(username, channel.json_body["displayname"])
# ensure the email from the OIDC response has not been configured for the user.
channel = self.make_request(
"GET", "/account/3pid", access_token=chan.json_body["access_token"]
)
self.assertEqual(channel.code, 200, channel.result)
self.assertListEqual([], channel.json_body["threepids"])
async def mock_get_file(
url: str,
output_stream: BinaryIO,
max_size: Optional[int] = None,
headers: Optional[RawHeaders] = None,
is_allowed_content_type: Optional[Callable[[str], bool]] = None,
) -> Tuple[int, Dict[bytes, List[bytes]], str, int]:
return 0, {b"Content-Type": [b"image/png"]}, "", 200
self.assertEqual(chan.json_body["user_id"], "@bobby:test")
-9
View File
@@ -22,7 +22,6 @@
import datetime
import os
from typing import Any, Dict, List, Tuple
from unittest.mock import AsyncMock
import pkg_resources
@@ -43,7 +42,6 @@ from synapse.types import JsonDict
from synapse.util import Clock
from tests import unittest
from tests.server import ThreadedMemoryReactorClock
from tests.unittest import override_config
@@ -60,13 +58,6 @@ class RegisterRestServletTestCase(unittest.HomeserverTestCase):
config["allow_guest_access"] = True
return config
def make_homeserver(
self, reactor: ThreadedMemoryReactorClock, clock: Clock
) -> HomeServer:
hs = super().make_homeserver(reactor, clock)
hs.get_send_email_handler()._sendmail = AsyncMock()
return hs
def test_POST_appservice_registration_valid(self) -> None:
user_id = "@as_user_kermit:test"
as_token = "i_am_an_app_service"
+1 -400
View File
@@ -2,7 +2,7 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2022 The Matrix.org Foundation C.I.C.
# Copyright (C) 2023-2024 New Vector, Ltd
# Copyright (C) 2023 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -19,14 +19,9 @@
#
#
from typing import Dict
from urllib.parse import urlparse
from twisted.test.proto_helpers import MemoryReactor
from twisted.web.resource import Resource
from synapse.rest.client import rendezvous
from synapse.rest.synapse.client.rendezvous import MSC4108RendezvousSessionResource
from synapse.server import HomeServer
from synapse.util import Clock
@@ -47,12 +42,6 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase):
self.hs = self.setup_test_homeserver()
return self.hs
def create_resource_dict(self) -> Dict[str, Resource]:
return {
**super().create_resource_dict(),
"/_synapse/client/rendezvous": MSC4108RendezvousSessionResource(self.hs),
}
def test_disabled(self) -> None:
channel = self.make_request("POST", msc3886_endpoint, {}, access_token=None)
self.assertEqual(channel.code, 404)
@@ -86,391 +75,3 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase):
channel = self.make_request("POST", msc4108_endpoint, {}, access_token=None)
self.assertEqual(channel.code, 307)
self.assertEqual(channel.headers.getRawHeaders("Location"), ["https://asd"])
@unittest.skip_unless(HAS_AUTHLIB, "requires authlib")
@override_config(
{
"disable_registration": True,
"experimental_features": {
"msc4108_enabled": True,
"msc3861": {
"enabled": True,
"issuer": "https://issuer",
"client_id": "client_id",
"client_auth_method": "client_secret_post",
"client_secret": "client_secret",
"admin_token": "admin_token_value",
},
},
}
)
def test_msc4108(self) -> None:
"""
Test the MSC4108 rendezvous endpoint, including:
- Creating a session
- Getting the data back
- Updating the data
- Deleting the data
- ETag handling
"""
# We can post arbitrary data to the endpoint
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
self.assertSubstring("/_synapse/client/rendezvous/", channel.json_body["url"])
headers = dict(channel.headers.getAllRawHeaders())
self.assertIn(b"ETag", headers)
self.assertIn(b"Expires", headers)
self.assertEqual(headers[b"Content-Type"], [b"application/json"])
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
self.assertIn("url", channel.json_body)
self.assertTrue(channel.json_body["url"].startswith("https://"))
url = urlparse(channel.json_body["url"])
session_endpoint = url.path
etag = headers[b"ETag"][0]
# We can get the data back
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 200)
headers = dict(channel.headers.getAllRawHeaders())
self.assertEqual(headers[b"ETag"], [etag])
self.assertIn(b"Expires", headers)
self.assertEqual(headers[b"Content-Type"], [b"text/plain"])
self.assertEqual(headers[b"Access-Control-Allow-Origin"], [b"*"])
self.assertEqual(headers[b"Access-Control-Expose-Headers"], [b"etag"])
self.assertEqual(headers[b"Cache-Control"], [b"no-store"])
self.assertEqual(headers[b"Pragma"], [b"no-cache"])
self.assertEqual(channel.text_body, "foo=bar")
# We can make sure the data hasn't changed
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
custom_headers=[("If-None-Match", etag)],
)
self.assertEqual(channel.code, 304)
# We can update the data
channel = self.make_request(
"PUT",
session_endpoint,
"foo=baz",
content_type=b"text/plain",
access_token=None,
custom_headers=[("If-Match", etag)],
)
self.assertEqual(channel.code, 202)
headers = dict(channel.headers.getAllRawHeaders())
old_etag = etag
new_etag = headers[b"ETag"][0]
# If we try to update it again with the old etag, it should fail
channel = self.make_request(
"PUT",
session_endpoint,
"bar=baz",
content_type=b"text/plain",
access_token=None,
custom_headers=[("If-Match", old_etag)],
)
self.assertEqual(channel.code, 412)
self.assertEqual(channel.json_body["errcode"], "M_UNKNOWN")
self.assertEqual(
channel.json_body["org.matrix.msc4108.errcode"], "M_CONCURRENT_WRITE"
)
# If we try to get with the old etag, we should get the updated data
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
custom_headers=[("If-None-Match", old_etag)],
)
self.assertEqual(channel.code, 200)
headers = dict(channel.headers.getAllRawHeaders())
self.assertEqual(headers[b"ETag"], [new_etag])
self.assertEqual(channel.text_body, "foo=baz")
# We can delete the data
channel = self.make_request(
"DELETE",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 204)
# If we try to get the data again, it should fail
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 404)
self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND")
@unittest.skip_unless(HAS_AUTHLIB, "requires authlib")
@override_config(
{
"disable_registration": True,
"experimental_features": {
"msc4108_enabled": True,
"msc3861": {
"enabled": True,
"issuer": "https://issuer",
"client_id": "client_id",
"client_auth_method": "client_secret_post",
"client_secret": "client_secret",
"admin_token": "admin_token_value",
},
},
}
)
def test_msc4108_expiration(self) -> None:
"""
Test that entries are evicted after a TTL.
"""
# Start a new session
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
session_endpoint = urlparse(channel.json_body["url"]).path
# Sanity check that we can get the data back
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 200)
self.assertEqual(channel.text_body, "foo=bar")
# Advance the clock, TTL of entries is 1 minute
self.reactor.advance(60)
# Get the data back, it should be gone
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 404)
@unittest.skip_unless(HAS_AUTHLIB, "requires authlib")
@override_config(
{
"disable_registration": True,
"experimental_features": {
"msc4108_enabled": True,
"msc3861": {
"enabled": True,
"issuer": "https://issuer",
"client_id": "client_id",
"client_auth_method": "client_secret_post",
"client_secret": "client_secret",
"admin_token": "admin_token_value",
},
},
}
)
def test_msc4108_capacity(self) -> None:
"""
Test that a capacity limit is enforced on the rendezvous sessions, as old
entries are evicted at an interval when the limit is reached.
"""
# Start a new session
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
session_endpoint = urlparse(channel.json_body["url"]).path
# Sanity check that we can get the data back
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 200)
self.assertEqual(channel.text_body, "foo=bar")
# Start a lot of new sessions
for _ in range(100):
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
# Get the data back, it should still be there, as the eviction hasn't run yet
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 200)
# Advance the clock, as it will trigger the eviction
self.reactor.advance(1)
# Get the data back, it should be gone
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
@unittest.skip_unless(HAS_AUTHLIB, "requires authlib")
@override_config(
{
"disable_registration": True,
"experimental_features": {
"msc4108_enabled": True,
"msc3861": {
"enabled": True,
"issuer": "https://issuer",
"client_id": "client_id",
"client_auth_method": "client_secret_post",
"client_secret": "client_secret",
"admin_token": "admin_token_value",
},
},
}
)
def test_msc4108_hard_capacity(self) -> None:
"""
Test that a hard capacity limit is enforced on the rendezvous sessions, as old
entries are evicted immediately when the limit is reached.
"""
# Start a new session
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
session_endpoint = urlparse(channel.json_body["url"]).path
# We advance the clock to make sure that this entry is the "lowest" in the session list
self.reactor.advance(1)
# Sanity check that we can get the data back
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 200)
self.assertEqual(channel.text_body, "foo=bar")
# Start a lot of new sessions
for _ in range(200):
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
# Get the data back, it should already be gone as we hit the hard limit
channel = self.make_request(
"GET",
session_endpoint,
access_token=None,
)
self.assertEqual(channel.code, 404)
@unittest.skip_unless(HAS_AUTHLIB, "requires authlib")
@override_config(
{
"disable_registration": True,
"experimental_features": {
"msc4108_enabled": True,
"msc3861": {
"enabled": True,
"issuer": "https://issuer",
"client_id": "client_id",
"client_auth_method": "client_secret_post",
"client_secret": "client_secret",
"admin_token": "admin_token_value",
},
},
}
)
def test_msc4108_content_type(self) -> None:
"""
Test that the content-type is restricted to text/plain.
"""
# We cannot post invalid content-type arbitrary data to the endpoint
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_is_form=True,
access_token=None,
)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM")
# Make a valid request
channel = self.make_request(
"POST",
msc4108_endpoint,
"foo=bar",
content_type=b"text/plain",
access_token=None,
)
self.assertEqual(channel.code, 201)
url = urlparse(channel.json_body["url"])
session_endpoint = url.path
headers = dict(channel.headers.getAllRawHeaders())
etag = headers[b"ETag"][0]
# We can't update the data with invalid content-type
channel = self.make_request(
"PUT",
session_endpoint,
"foo=baz",
content_is_form=True,
access_token=None,
custom_headers=[("If-Match", etag)],
)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM")
+1 -6
View File
@@ -163,12 +163,7 @@ class RetentionTestCase(unittest.HomeserverTestCase):
)
self.assertEqual(2, len(events), "events retrieved from database")
filtered_events = self.get_success(
filter_events_for_client(
storage_controllers,
self.user_id,
events,
msc4115_membership_on_events=True,
)
filter_events_for_client(storage_controllers, self.user_id, events)
)
# We should only get one event back.
+3 -66
View File
@@ -48,16 +48,7 @@ from synapse.appservice import ApplicationService
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.rest import admin
from synapse.rest.client import (
account,
directory,
knock,
login,
profile,
register,
room,
sync,
)
from synapse.rest.client import account, directory, login, profile, register, room, sync
from synapse.server import HomeServer
from synapse.types import JsonDict, RoomAlias, UserID, create_requester
from synapse.util import Clock
@@ -742,7 +733,7 @@ class RoomsCreateTestCase(RoomBase):
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
self.assertTrue("room_id" in channel.json_body)
assert channel.resource_usage is not None
self.assertEqual(33, channel.resource_usage.db_txn_count)
self.assertEqual(32, channel.resource_usage.db_txn_count)
def test_post_room_initial_state(self) -> None:
# POST with initial_state config key, expect new room id
@@ -755,7 +746,7 @@ class RoomsCreateTestCase(RoomBase):
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
self.assertTrue("room_id" in channel.json_body)
assert channel.resource_usage is not None
self.assertEqual(35, channel.resource_usage.db_txn_count)
self.assertEqual(34, channel.resource_usage.db_txn_count)
def test_post_room_visibility_key(self) -> None:
# POST with visibility config key, expect new room id
@@ -1163,7 +1154,6 @@ class RoomJoinTestCase(RoomBase):
admin.register_servlets,
login.register_servlets,
room.register_servlets,
knock.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
@@ -1177,8 +1167,6 @@ class RoomJoinTestCase(RoomBase):
self.room2 = self.helper.create_room_as(room_creator=self.user1, tok=self.tok1)
self.room3 = self.helper.create_room_as(room_creator=self.user1, tok=self.tok1)
self.store = hs.get_datastores().main
def test_spam_checker_may_join_room_deprecated(self) -> None:
"""Tests that the user_may_join_room spam checker callback is correctly called
and blocks room joins when needed.
@@ -1329,57 +1317,6 @@ class RoomJoinTestCase(RoomBase):
expect_additional_fields=return_value[1],
)
def test_suspended_user_cannot_join_room(self) -> None:
# set the user as suspended
self.get_success(self.store.set_user_suspended_status(self.user2, True))
channel = self.make_request(
"POST", f"/join/{self.room1}", access_token=self.tok2
)
self.assertEqual(channel.code, 403)
self.assertEqual(
channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
)
channel = self.make_request(
"POST", f"/rooms/{self.room1}/join", access_token=self.tok2
)
self.assertEqual(channel.code, 403)
self.assertEqual(
channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
)
def test_suspended_user_cannot_knock_on_room(self) -> None:
# set the user as suspended
self.get_success(self.store.set_user_suspended_status(self.user2, True))
channel = self.make_request(
"POST",
f"/_matrix/client/v3/knock/{self.room1}",
access_token=self.tok2,
content={},
shorthand=False,
)
self.assertEqual(channel.code, 403)
self.assertEqual(
channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
)
def test_suspended_user_cannot_invite_to_room(self) -> None:
# set the user as suspended
self.get_success(self.store.set_user_suspended_status(self.user1, True))
# first user invites second user
channel = self.make_request(
"POST",
f"/rooms/{self.room1}/invite",
access_token=self.tok1,
content={"user_id": self.user2},
)
self.assertEqual(
channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
)
class RoomAppserviceTsParamTestCase(unittest.HomeserverTestCase):
servlets = [
+1 -6
View File
@@ -351,7 +351,6 @@ def make_request(
request: Type[Request] = SynapseRequest,
shorthand: bool = True,
federation_auth_origin: Optional[bytes] = None,
content_type: Optional[bytes] = None,
content_is_form: bool = False,
await_result: bool = True,
custom_headers: Optional[Iterable[CustomHeaderType]] = None,
@@ -374,8 +373,6 @@ def make_request(
with the usual REST API path, if it doesn't contain it.
federation_auth_origin: if set to not-None, we will add a fake
Authorization header pretenting to be the given server name.
content_type: The content-type to use for the request. If not set then will default to
application/json unless content_is_form is true.
content_is_form: Whether the content is URL encoded form data. Adds the
'Content-Type': 'application/x-www-form-urlencoded' header.
await_result: whether to wait for the request to complete rendering. If true,
@@ -439,9 +436,7 @@ def make_request(
)
if content:
if content_type is not None:
req.requestHeaders.addRawHeader(b"Content-Type", content_type)
elif content_is_form:
if content_is_form:
req.requestHeaders.addRawHeader(
b"Content-Type", b"application/x-www-form-urlencoded"
)
+31 -73
View File
@@ -21,8 +21,6 @@
from typing import Dict, List, Set, Tuple, cast
from parameterized import parameterized
from twisted.test.proto_helpers import MemoryReactor
from twisted.trial import unittest
@@ -47,8 +45,7 @@ class EventChainStoreTestCase(HomeserverTestCase):
self.store = hs.get_datastores().main
self._next_stream_ordering = 1
@parameterized.expand([(False,), (True,)])
def test_simple(self, batched: bool) -> None:
def test_simple(self) -> None:
"""Test that the example in `docs/auth_chain_difference_algorithm.md`
works.
"""
@@ -56,7 +53,6 @@ class EventChainStoreTestCase(HomeserverTestCase):
event_factory = self.hs.get_event_builder_factory()
bob = "@creator:test"
alice = "@alice:test"
charlie = "@charlie:test"
room_id = "!room:test"
# Ensure that we have a rooms entry so that we generate the chain index.
@@ -195,26 +191,6 @@ class EventChainStoreTestCase(HomeserverTestCase):
)
)
charlie_invite = self.get_success(
event_factory.for_room_version(
RoomVersions.V6,
{
"type": EventTypes.Member,
"state_key": charlie,
"sender": alice,
"room_id": room_id,
"content": {"tag": "charlie_invite"},
},
).build(
prev_event_ids=[],
auth_event_ids=[
create.event_id,
alice_join2.event_id,
power_2.event_id,
],
)
)
events = [
create,
bob_join,
@@ -224,41 +200,33 @@ class EventChainStoreTestCase(HomeserverTestCase):
bob_join_2,
power_2,
alice_join2,
charlie_invite,
]
expected_links = [
(bob_join, create),
(power, create),
(power, bob_join),
(alice_invite, create),
(alice_invite, power),
(alice_invite, bob_join),
(bob_join_2, power),
(alice_join2, power_2),
(charlie_invite, alice_join2),
]
# We either persist as a batch or one-by-one depending on test
# parameter.
if batched:
self.persist(events)
else:
for event in events:
self.persist([event])
self.persist(events)
chain_map, link_map = self.fetch_chains(events)
# Check that the expected links and only the expected links have been
# added.
event_map = {e.event_id: e for e in events}
reverse_chain_map = {v: event_map[k] for k, v in chain_map.items()}
self.assertEqual(len(expected_links), len(list(link_map.get_additions())))
self.maxDiff = None
self.assertCountEqual(
expected_links,
[
(reverse_chain_map[(s1, s2)], reverse_chain_map[(t1, t2)])
for s1, s2, t1, t2 in link_map.get_additions()
],
)
for start, end in expected_links:
start_id, start_seq = chain_map[start.event_id]
end_id, end_seq = chain_map[end.event_id]
self.assertIn(
(start_seq, end_seq), list(link_map.get_links_between(start_id, end_id))
)
# Test that everything can reach the create event, but the create event
# can't reach anything.
@@ -400,23 +368,24 @@ class EventChainStoreTestCase(HomeserverTestCase):
expected_links = [
(bob_join, create),
(power, create),
(power, bob_join),
(alice_invite, create),
(alice_invite, power),
(alice_invite, bob_join),
]
# Check that the expected links and only the expected links have been
# added.
event_map = {e.event_id: e for e in events}
reverse_chain_map = {v: event_map[k] for k, v in chain_map.items()}
self.assertEqual(len(expected_links), len(list(link_map.get_additions())))
self.maxDiff = None
self.assertCountEqual(
expected_links,
[
(reverse_chain_map[(s1, s2)], reverse_chain_map[(t1, t2)])
for s1, s2, t1, t2 in link_map.get_additions()
],
)
for start, end in expected_links:
start_id, start_seq = chain_map[start.event_id]
end_id, end_seq = chain_map[end.event_id]
self.assertIn(
(start_seq, end_seq), list(link_map.get_links_between(start_id, end_id))
)
def persist(
self,
@@ -520,6 +489,8 @@ class LinkMapTestCase(unittest.TestCase):
link_map = _LinkMap()
link_map.add_link((1, 1), (2, 1), new=False)
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1)])
self.assertCountEqual(link_map.get_links_from((1, 1)), [(2, 1)])
self.assertCountEqual(link_map.get_additions(), [])
self.assertTrue(link_map.exists_path_from((1, 5), (2, 1)))
self.assertFalse(link_map.exists_path_from((1, 5), (2, 2)))
@@ -528,31 +499,18 @@ class LinkMapTestCase(unittest.TestCase):
# Attempting to add a redundant link is ignored.
self.assertFalse(link_map.add_link((1, 4), (2, 1)))
self.assertCountEqual(link_map.get_additions(), [])
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1)])
# Adding new non-redundant links works
self.assertTrue(link_map.add_link((1, 3), (2, 3)))
self.assertCountEqual(link_map.get_additions(), [(1, 3, 2, 3)])
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1), (3, 3)])
self.assertTrue(link_map.add_link((2, 5), (1, 3)))
self.assertCountEqual(link_map.get_links_between(2, 1), [(5, 3)])
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1), (3, 3)])
self.assertCountEqual(link_map.get_additions(), [(1, 3, 2, 3), (2, 5, 1, 3)])
def test_exists_path_from(self) -> None:
"Check that `exists_path_from` can handle non-direct links"
link_map = _LinkMap()
link_map.add_link((1, 1), (2, 1), new=False)
link_map.add_link((2, 1), (3, 1), new=False)
self.assertTrue(link_map.exists_path_from((1, 4), (3, 1)))
self.assertFalse(link_map.exists_path_from((1, 4), (3, 2)))
link_map.add_link((1, 5), (2, 3), new=False)
link_map.add_link((2, 2), (3, 3), new=False)
self.assertTrue(link_map.exists_path_from((1, 6), (3, 2)))
self.assertFalse(link_map.exists_path_from((1, 4), (3, 2)))
class EventChainBackgroundUpdateTestCase(HomeserverTestCase):
servlets = [
+1 -1
View File
@@ -43,6 +43,7 @@ class RegistrationStoreTestCase(HomeserverTestCase):
self.assertEqual(
UserInfo(
# TODO(paul): Surely this field should be 'user_id', not 'name'
user_id=UserID.from_string(self.user_id),
is_admin=False,
is_guest=False,
@@ -56,7 +57,6 @@ class RegistrationStoreTestCase(HomeserverTestCase):
locked=False,
is_shadow_banned=False,
approved=True,
suspended=False,
),
(self.get_success(self.store.get_user_by_id(self.user_id))),
)
+7 -6
View File
@@ -71,16 +71,17 @@ class EventSearchInsertionTest(HomeserverTestCase):
store.search_msgs([room_id], "hi bob", ["content.body"])
)
self.assertEqual(result.get("count"), 1)
self.assertIn("hi", result.get("highlights"))
self.assertIn("bob", result.get("highlights"))
if isinstance(store.database_engine, PostgresEngine):
self.assertIn("hi", result.get("highlights"))
self.assertIn("bob", result.get("highlights"))
# Check that search works for an unrelated message
result = self.get_success(
store.search_msgs([room_id], "another", ["content.body"])
)
self.assertEqual(result.get("count"), 1)
self.assertIn("another", result.get("highlights"))
if isinstance(store.database_engine, PostgresEngine):
self.assertIn("another", result.get("highlights"))
# Check that search works for a search term that overlaps with the message
# containing a null byte and an unrelated message.
@@ -89,8 +90,8 @@ class EventSearchInsertionTest(HomeserverTestCase):
result = self.get_success(
store.search_msgs([room_id], "hi alice", ["content.body"])
)
self.assertIn("alice", result.get("highlights"))
if isinstance(store.database_engine, PostgresEngine):
self.assertIn("alice", result.get("highlights"))
def test_non_string(self) -> None:
"""Test that non-string `value`s are not inserted into `event_search`.

Some files were not shown because too many files have changed in this diff Show More