Merge branch 'release-v1.86' into matrix-org-hotfixes
commit
98a00339a5
|
@ -22,7 +22,21 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check_repo:
|
||||
# Prevent this workflow from running on any fork of Synapse other than matrix-org/synapse, as it is
|
||||
# only useful to the Synapse core team.
|
||||
# All other workflow steps depend on this one, thus if 'should_run_workflow' is not 'true', the rest
|
||||
# of the workflow will be skipped as well.
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run_workflow: ${{ steps.check_condition.outputs.should_run_workflow }}
|
||||
steps:
|
||||
- id: check_condition
|
||||
run: echo "should_run_workflow=${{ github.repository == 'matrix-org/synapse' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mypy:
|
||||
needs: check_repo
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -47,6 +61,8 @@ jobs:
|
|||
run: sed '/warn_unused_ignores = True/d' -i mypy.ini
|
||||
- run: poetry run mypy
|
||||
trial:
|
||||
needs: check_repo
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
@ -105,6 +121,8 @@ jobs:
|
|||
|
||||
|
||||
sytest:
|
||||
needs: check_repo
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: matrixdotorg/sytest-synapse:testing
|
||||
|
@ -156,7 +174,8 @@ jobs:
|
|||
|
||||
|
||||
complement:
|
||||
if: "${{ !failure() && !cancelled() }}"
|
||||
needs: check_repo
|
||||
if: "!failure() && !cancelled() && needs.check_repo.outputs.should_run_workflow == 'true'"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
|
@ -192,7 +211,7 @@ jobs:
|
|||
# Open an issue if the build fails, so we know about it.
|
||||
# Only do this if we're not experimenting with this action in a PR.
|
||||
open-issue:
|
||||
if: "failure() && github.event_name != 'push' && github.event_name != 'pull_request'"
|
||||
if: "failure() && github.event_name != 'push' && github.event_name != 'pull_request' && needs.check_repo.outputs.should_run_workflow == 'true'"
|
||||
needs:
|
||||
# TODO: should mypy be included here? It feels more brittle than the others.
|
||||
- mypy
|
||||
|
|
|
@ -34,6 +34,7 @@ jobs:
|
|||
- id: set-distros
|
||||
run: |
|
||||
# if we're running from a tag, get the full list of distros; otherwise just use debian:sid
|
||||
# NOTE: inside the actual Dockerfile-dhvirtualenv, the image name is expanded into its full image path
|
||||
dists='["debian:sid"]'
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
dists=$(scripts-dev/build_debian_packages.py --show-dists-json)
|
||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
|
@ -92,6 +92,10 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Poetry
|
||||
uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
|
@ -103,10 +107,6 @@ jobs:
|
|||
# To make CI green, err towards caution and install the project.
|
||||
install-project: "true"
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
# Cribbed from
|
||||
# https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17
|
||||
- name: Restore/persist mypy's cache
|
||||
|
@ -150,7 +150,7 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
with:
|
||||
|
@ -167,7 +167,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
@ -268,7 +268,7 @@ jobs:
|
|||
postgres:${{ matrix.job.postgres-version }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: matrix-org/setup-python-poetry@v1
|
||||
|
@ -308,7 +308,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
# There aren't wheels for some of the older deps, so we need to install
|
||||
|
@ -416,7 +416,7 @@ jobs:
|
|||
run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run SyTest
|
||||
|
@ -556,7 +556,7 @@ jobs:
|
|||
path: synapse
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
|
@ -584,7 +584,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.58.1
|
||||
uses: dtolnay/rust-toolchain@1.60.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- run: cargo test
|
||||
|
|
|
@ -18,7 +18,22 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check_repo:
|
||||
# Prevent this workflow from running on any fork of Synapse other than matrix-org/synapse, as it is
|
||||
# only useful to the Synapse core team.
|
||||
# All other workflow steps depend on this one, thus if 'should_run_workflow' is not 'true', the rest
|
||||
# of the workflow will be skipped as well.
|
||||
if: github.repository == 'matrix-org/synapse'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run_workflow: ${{ steps.check_condition.outputs.should_run_workflow }}
|
||||
steps:
|
||||
- id: check_condition
|
||||
run: echo "should_run_workflow=${{ github.repository == 'matrix-org/synapse' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mypy:
|
||||
needs: check_repo
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -41,6 +56,8 @@ jobs:
|
|||
- run: poetry run mypy
|
||||
|
||||
trial:
|
||||
needs: check_repo
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -75,6 +92,8 @@ jobs:
|
|||
|| true
|
||||
|
||||
sytest:
|
||||
needs: check_repo
|
||||
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: matrixdotorg/sytest-synapse:buster
|
||||
|
@ -119,7 +138,8 @@ jobs:
|
|||
/logs/**/*.log*
|
||||
|
||||
complement:
|
||||
if: "${{ !failure() && !cancelled() }}"
|
||||
needs: check_repo
|
||||
if: "!failure() && !cancelled() && needs.check_repo.outputs.should_run_workflow == 'true'"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
|
@ -166,7 +186,7 @@ jobs:
|
|||
|
||||
# open an issue if the build fails, so we know about it.
|
||||
open-issue:
|
||||
if: failure()
|
||||
if: failure() && needs.check_repo.outputs.should_run_workflow == 'true'
|
||||
needs:
|
||||
- mypy
|
||||
- trial
|
||||
|
|
62
CHANGES.md
62
CHANGES.md
|
@ -1,3 +1,65 @@
|
|||
Synapse 1.85.2 (2023-06-08)
|
||||
===========================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix regression where using TLS for HTTP replication between workers did not work. Introduced in v1.85.0. ([\#15746](https://github.com/matrix-org/synapse/issues/15746))
|
||||
|
||||
|
||||
Synapse 1.85.1 (2023-06-07)
|
||||
===========================
|
||||
|
||||
Note: this release only fixes a bug that stopped some deployments from upgrading to v1.85.0. There is no need to upgrade to v1.85.1 if successfully running v1.85.0.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix bug in schema delta that broke upgrades for some deployments. Introduced in v1.85.0. ([\#15738](https://github.com/matrix-org/synapse/issues/15738), [\#15739](https://github.com/matrix-org/synapse/issues/15739))
|
||||
|
||||
|
||||
Synapse 1.85.0 (2023-06-06)
|
||||
===========================
|
||||
|
||||
No significant changes since 1.85.0rc2.
|
||||
|
||||
|
||||
## Security advisory
|
||||
|
||||
The following issues are fixed in 1.85.0 (and RCs).
|
||||
|
||||
- [GHSA-26c5-ppr8-f33p](https://github.com/matrix-org/synapse/security/advisories/GHSA-26c5-ppr8-f33p) / [CVE-2023-32682](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-32682) — Low Severity
|
||||
|
||||
It may be possible for a deactivated user to login when using uncommon configurations.
|
||||
|
||||
- [GHSA-98px-6486-j7qc](https://github.com/matrix-org/synapse/security/advisories/GHSA-98px-6486-j7qc) / [CVE-2023-32683](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-32683) — Low Severity
|
||||
|
||||
A discovered oEmbed or image URL can bypass the `url_preview_url_blacklist` setting potentially allowing server side request forgery or bypassing network policies. Impact is limited to IP addresses allowed by the `url_preview_ip_range_blacklist` setting (by default this only allows public IPs).
|
||||
|
||||
See the advisories for more details. If you have any questions, email security@matrix.org.
|
||||
|
||||
|
||||
Synapse 1.85.0rc2 (2023-06-01)
|
||||
==============================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a performance issue introduced in Synapse v1.83.0 which meant that purging rooms was very slow and database-intensive. ([\#15693](https://github.com/matrix-org/synapse/issues/15693))
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Deprecate calling the `/register` endpoint with an unspecced `user` property for application services. ([\#15703](https://github.com/matrix-org/synapse/issues/15703))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Speed up background jobs `populate_full_user_id_user_filters` and `populate_full_user_id_profiles`. ([\#15700](https://github.com/matrix-org/synapse/issues/15700))
|
||||
|
||||
|
||||
Synapse 1.85.0rc1 (2023-05-30)
|
||||
==============================
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ version = 3
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.19"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
|
||||
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -132,9 +132,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.18"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
|
@ -229,9 +229,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-log"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c8b57fe71fb5dcf38970ebedc2b1531cf1c14b1b9b4c560a182a57e115575c"
|
||||
checksum = "c94ff6535a6bae58d7d0b85e60d4c53f7f84d0d0aa35d6a28c3f3e70bfe51444"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"log",
|
||||
|
@ -291,9 +291,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.3"
|
||||
version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
|
||||
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -302,9 +302,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
|
@ -320,18 +320,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.163"
|
||||
version = "1.0.164"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
||||
checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.163"
|
||||
version = "1.0.164"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
||||
checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Allow for the configuration of max request retries and min/max retry delays in the matrix federation client.
|
|
@ -0,0 +1 @@
|
|||
Log when events are (maybe unexpectedly) filtered out of responses in tests.
|
|
@ -0,0 +1 @@
|
|||
Stable support for [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882) to allow an existing device/session to generate a login token for use on a new device/session.
|
|
@ -0,0 +1 @@
|
|||
Support resolving a room's [canonical alias](https://spec.matrix.org/v1.7/client-server-api/#mroomcanonical_alias) via the module API.
|
|
@ -0,0 +1 @@
|
|||
Enable support for [MSC3952](https://github.com/matrix-org/matrix-spec-proposals/pull/3952): intentional mentions.
|
|
@ -0,0 +1 @@
|
|||
Experimental [MSC3861](https://github.com/matrix-org/matrix-spec-proposals/pull/3861) support: delegate auth to an OIDC provider.
|
|
@ -0,0 +1 @@
|
|||
Correctly clear caches when we delete a room.
|
|
@ -0,0 +1 @@
|
|||
Read from column `full_user_id` rather than `user_id` of tables `profiles` and `user_filters`.
|
|
@ -0,0 +1 @@
|
|||
Add support for tracing functions which return `Awaitable`s.
|
|
@ -0,0 +1 @@
|
|||
Add Syanpse version deploy annotations to Grafana dashboard which enables easy correlation between behavior changes witnessed in a graph to a certain Synapse version and nail down regressions.
|
|
@ -0,0 +1 @@
|
|||
Cache requests for user's devices over federation.
|
|
@ -0,0 +1 @@
|
|||
Add fully qualified docker image names to Dockerfiles.
|
|
@ -0,0 +1 @@
|
|||
Remove some unused code.
|
|
@ -1 +0,0 @@
|
|||
Fix a performance issue introduced in Synapse v1.83.0 which meant that purging rooms was very slow and database-intensive.
|
|
@ -0,0 +1 @@
|
|||
Improve type hints.
|
|
@ -0,0 +1 @@
|
|||
Check permissions for enabling encryption earlier during room creation to avoid creating broken rooms.
|
|
@ -0,0 +1 @@
|
|||
Improve type hints.
|
|
@ -1 +0,0 @@
|
|||
Speed up background jobs `populate_full_user_id_user_filters` and `populate_full_user_id_profiles`.
|
|
@ -0,0 +1 @@
|
|||
Add a catch-all * to the supported relation types when redacting an event and its related events. This is an update to [MSC3912](https://github.com/matrix-org/matrix-spec-proposals/pull/3861) implementation.
|
|
@ -0,0 +1 @@
|
|||
Update docstring and traces on `maybe_backfill()` functions.
|
|
@ -0,0 +1 @@
|
|||
Speed up `/messages` by backfilling in the background when there are no backward extremities where we are directly paginating.
|
|
@ -0,0 +1 @@
|
|||
Add context for when/why to use the `long_retries` option when sending Federation requests.
|
|
@ -0,0 +1 @@
|
|||
Removed some unused fields.
|
|
@ -0,0 +1 @@
|
|||
Update federation error to more plainly explain we can only authorize our own membership events.
|
|
@ -0,0 +1 @@
|
|||
Prevent the `latest_deps` and `twisted_trunk` daily GitHub Actions workflows from running on forks of the codebase.
|
|
@ -0,0 +1 @@
|
|||
Improve performance of user directory search.
|
|
@ -0,0 +1 @@
|
|||
Remove redundant table join with `room_memberships` when doing a `is_host_joined()`/`is_host_invited()` call (`membership` is already part of the `current_state_events`).
|
|
@ -0,0 +1 @@
|
|||
Simplify query to find participating servers in a room.
|
|
@ -0,0 +1 @@
|
|||
Remove superfluous `room_memberships` join from background update.
|
|
@ -0,0 +1 @@
|
|||
Expose a metric reporting the database background update status.
|
|
@ -0,0 +1 @@
|
|||
Speed up typechecking CI.
|
|
@ -0,0 +1 @@
|
|||
Bump minimum supported Rust version to 1.60.0.
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,27 @@
|
|||
matrix-synapse-py3 (1.85.2) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.85.2.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Thu, 08 Jun 2023 13:04:18 +0100
|
||||
|
||||
matrix-synapse-py3 (1.85.1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.85.1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Wed, 07 Jun 2023 10:51:12 +0100
|
||||
|
||||
matrix-synapse-py3 (1.85.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.85.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 06 Jun 2023 09:39:29 +0100
|
||||
|
||||
matrix-synapse-py3 (1.85.0~rc2) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.85.0rc2.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Thu, 01 Jun 2023 09:16:18 -0700
|
||||
|
||||
matrix-synapse-py3 (1.85.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.85.0rc1.
|
||||
|
|
|
@ -27,7 +27,7 @@ ARG PYTHON_VERSION=3.11
|
|||
###
|
||||
# We hardcode the use of Debian bullseye here because this could change upstream
|
||||
# and other Dockerfiles used for testing are expecting bullseye.
|
||||
FROM docker.io/python:${PYTHON_VERSION}-slim-bullseye as requirements
|
||||
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bullseye as requirements
|
||||
|
||||
# RUN --mount is specific to buildkit and is documented at
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#build-mounts-run---mount.
|
||||
|
@ -87,7 +87,7 @@ RUN if [ -z "$TEST_ONLY_IGNORE_POETRY_LOCKFILE" ]; then \
|
|||
###
|
||||
### Stage 1: builder
|
||||
###
|
||||
FROM docker.io/python:${PYTHON_VERSION}-slim-bullseye as builder
|
||||
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bullseye as builder
|
||||
|
||||
# install the OS build deps
|
||||
RUN \
|
||||
|
@ -158,7 +158,7 @@ RUN --mount=type=cache,target=/synapse/target,sharing=locked \
|
|||
### Stage 2: runtime
|
||||
###
|
||||
|
||||
FROM docker.io/python:${PYTHON_VERSION}-slim-bullseye
|
||||
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bullseye
|
||||
|
||||
LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse'
|
||||
LABEL org.opencontainers.image.documentation='https://github.com/matrix-org/synapse/blob/master/docker/README.md'
|
||||
|
|
|
@ -24,7 +24,7 @@ ARG distro=""
|
|||
# https://launchpad.net/~jyrki-pulliainen/+archive/ubuntu/dh-virtualenv, but
|
||||
# it's not obviously easier to use that than to build our own.)
|
||||
|
||||
FROM ${distro} as builder
|
||||
FROM docker.io/library/${distro} as builder
|
||||
|
||||
RUN apt-get update -qq -o Acquire::Languages=none
|
||||
RUN env DEBIAN_FRONTEND=noninteractive apt-get install \
|
||||
|
@ -55,7 +55,7 @@ RUN cd /dh-virtualenv && DEB_BUILD_OPTIONS=nodoc dpkg-buildpackage -us -uc -b
|
|||
###
|
||||
### Stage 1
|
||||
###
|
||||
FROM ${distro}
|
||||
FROM docker.io/library/${distro}
|
||||
|
||||
# Get the distro we want to pull from as a dynamic build variable
|
||||
# (We need to define it in each build stage)
|
||||
|
|
|
@ -7,7 +7,7 @@ ARG FROM=matrixdotorg/synapse:$SYNAPSE_VERSION
|
|||
# target image. For repeated rebuilds, this is much faster than apt installing
|
||||
# each time.
|
||||
|
||||
FROM debian:bullseye-slim AS deps_base
|
||||
FROM docker.io/library/debian:bullseye-slim AS deps_base
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
|
@ -21,7 +21,7 @@ FROM debian:bullseye-slim AS deps_base
|
|||
# which makes it much easier to copy (but we need to make sure we use an image
|
||||
# based on the same debian version as the synapse image, to make sure we get
|
||||
# the expected version of libc.
|
||||
FROM redis:6-bullseye AS redis_base
|
||||
FROM docker.io/library/redis:7-bullseye AS redis_base
|
||||
|
||||
# now build the final image, based on the the regular Synapse docker image
|
||||
FROM $FROM
|
||||
|
|
|
@ -73,7 +73,8 @@ The following environment variables are supported in `generate` mode:
|
|||
will log sensitive information such as access tokens.
|
||||
This should not be needed unless you are a developer attempting to debug something
|
||||
particularly tricky.
|
||||
|
||||
* `SYNAPSE_LOG_TESTING`: if set, Synapse will log additional information useful
|
||||
for testing.
|
||||
|
||||
## Postgres
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
# https://github.com/matrix-org/synapse/blob/develop/docker/README-testing.md#testing-with-postgresql-and-single-or-multi-process-synapse
|
||||
|
||||
ARG SYNAPSE_VERSION=latest
|
||||
# This is an intermediate image, to be built locally (not pulled from a registry).
|
||||
ARG FROM=matrixdotorg/synapse-workers:$SYNAPSE_VERSION
|
||||
|
||||
FROM $FROM
|
||||
|
@ -19,8 +20,8 @@ FROM $FROM
|
|||
# the same debian version as Synapse's docker image (so the versions of the
|
||||
# shared libraries match).
|
||||
RUN adduser --system --uid 999 postgres --home /var/lib/postgresql
|
||||
COPY --from=postgres:13-bullseye /usr/lib/postgresql /usr/lib/postgresql
|
||||
COPY --from=postgres:13-bullseye /usr/share/postgresql /usr/share/postgresql
|
||||
COPY --from=docker.io/library/postgres:13-bullseye /usr/lib/postgresql /usr/lib/postgresql
|
||||
COPY --from=docker.io/library/postgres:13-bullseye /usr/share/postgresql /usr/share/postgresql
|
||||
RUN mkdir /var/run/postgresql && chown postgres /var/run/postgresql
|
||||
ENV PATH="${PATH}:/usr/lib/postgresql/13/bin"
|
||||
ENV PGDATA=/var/lib/postgresql/data
|
||||
|
|
|
@ -49,17 +49,35 @@ handlers:
|
|||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
{% if not SYNAPSE_LOG_SENSITIVE %}
|
||||
{#
|
||||
If SYNAPSE_LOG_SENSITIVE is unset, then override synapse.storage.SQL to INFO
|
||||
so that DEBUG entries (containing sensitive information) are not emitted.
|
||||
#}
|
||||
loggers:
|
||||
# This is just here so we can leave `loggers` in the config regardless of whether
|
||||
# we configure other loggers below (avoid empty yaml dict error).
|
||||
_placeholder:
|
||||
level: "INFO"
|
||||
|
||||
{% if not SYNAPSE_LOG_SENSITIVE %}
|
||||
{#
|
||||
If SYNAPSE_LOG_SENSITIVE is unset, then override synapse.storage.SQL to INFO
|
||||
so that DEBUG entries (containing sensitive information) are not emitted.
|
||||
#}
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: INFO
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if SYNAPSE_LOG_TESTING %}
|
||||
{#
|
||||
If Synapse is under test, log a few more useful things for a developer
|
||||
attempting to debug something particularly tricky.
|
||||
|
||||
With `synapse.visibility.filtered_event_debug`, it logs when events are (maybe
|
||||
unexpectedly) filtered out of responses in tests. It's just nice to be able to
|
||||
look at the CI log and figure out why an event isn't being returned.
|
||||
#}
|
||||
synapse.visibility.filtered_event_debug:
|
||||
level: DEBUG
|
||||
{% endif %}
|
||||
|
||||
root:
|
||||
level: {{ SYNAPSE_LOG_LEVEL or "INFO" }}
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
# log level. INFO is the default.
|
||||
# * SYNAPSE_LOG_SENSITIVE: If unset, SQL and SQL values won't be logged,
|
||||
# regardless of the SYNAPSE_LOG_LEVEL setting.
|
||||
# * SYNAPSE_LOG_TESTING: if set, Synapse will log additional information useful
|
||||
# for testing.
|
||||
#
|
||||
# NOTE: According to Complement's ENTRYPOINT expectations for a homeserver image (as defined
|
||||
# in the project's README), this script may be run multiple times, and functionality should
|
||||
|
@ -947,6 +949,7 @@ def generate_worker_log_config(
|
|||
extra_log_template_args["SYNAPSE_LOG_SENSITIVE"] = environ.get(
|
||||
"SYNAPSE_LOG_SENSITIVE"
|
||||
)
|
||||
extra_log_template_args["SYNAPSE_LOG_TESTING"] = environ.get("SYNAPSE_LOG_TESTING")
|
||||
|
||||
# Render and write the file
|
||||
log_config_filepath = f"/conf/workers/{worker_name}.log.config"
|
||||
|
|
|
@ -10,7 +10,7 @@ ARG PYTHON_VERSION=3.9
|
|||
###
|
||||
# We hardcode the use of Debian bullseye here because this could change upstream
|
||||
# and other Dockerfiles used for testing are expecting bullseye.
|
||||
FROM docker.io/python:${PYTHON_VERSION}-slim-bullseye
|
||||
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bullseye
|
||||
|
||||
# Install Rust and other dependencies (stolen from normal Dockerfile)
|
||||
# install the OS build deps
|
||||
|
|
|
@ -87,6 +87,25 @@ process, for example:
|
|||
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||
```
|
||||
# Upgrading to v1.86.0
|
||||
|
||||
## Minimum supported Rust version
|
||||
|
||||
The minimum supported Rust version has been increased from v1.58.1 to v1.60.0.
|
||||
Users building from source will need to ensure their `rustc` version is up to
|
||||
date.
|
||||
|
||||
|
||||
# Upgrading to v1.85.0
|
||||
|
||||
## Application service registration with "user" property deprecation
|
||||
|
||||
Application services should ensure they call the `/register` endpoint with a
|
||||
`username` property. The legacy `user` property is considered deprecated and
|
||||
should no longer be included.
|
||||
|
||||
A future version of Synapse (v1.88.0 or later) will remove support for legacy
|
||||
application service login.
|
||||
|
||||
# Upgrading to v1.84.0
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@ What servers are currently participating in this room?
|
|||
Run this sql query on your db:
|
||||
```sql
|
||||
SELECT DISTINCT split_part(state_key, ':', 2)
|
||||
FROM current_state_events AS c
|
||||
INNER JOIN room_memberships AS m USING (room_id, event_id)
|
||||
WHERE room_id = '!cURbafjkfsMDVwdRDQ:matrix.org' AND membership = 'join';
|
||||
FROM current_state_events
|
||||
WHERE room_id = '!cURbafjkfsMDVwdRDQ:matrix.org' AND membership = 'join';
|
||||
```
|
||||
|
||||
What users are registered on my server?
|
||||
|
|
|
@ -1196,6 +1196,32 @@ Example configuration:
|
|||
allow_device_name_lookup_over_federation: true
|
||||
```
|
||||
---
|
||||
### `federation`
|
||||
|
||||
The federation section defines some sub-options related to federation.
|
||||
|
||||
The following options are related to configuring timeout and retry logic for one request,
|
||||
independently of the others.
|
||||
Short retry algorithm is used when something or someone will wait for the request to have an
|
||||
answer, while long retry is used for requests that happen in the background,
|
||||
like sending a federation transaction.
|
||||
|
||||
* `client_timeout`: timeout for the federation requests in seconds. Default to 60s.
|
||||
* `max_short_retry_delay`: maximum delay to be used for the short retry algo in seconds. Default to 2s.
|
||||
* `max_long_retry_delay`: maximum delay to be used for the short retry algo in seconds. Default to 60s.
|
||||
* `max_short_retries`: maximum number of retries for the short retry algo. Default to 3 attempts.
|
||||
* `max_long_retries`: maximum number of retries for the long retry algo. Default to 10 attempts.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
federation:
|
||||
client_timeout: 180
|
||||
max_short_retry_delay: 7
|
||||
max_long_retry_delay: 100
|
||||
max_short_retries: 5
|
||||
max_long_retries: 20
|
||||
```
|
||||
---
|
||||
## Caching
|
||||
|
||||
Options related to caching.
|
||||
|
@ -2570,7 +2596,50 @@ Example configuration:
|
|||
```yaml
|
||||
nonrefreshable_access_token_lifetime: 24h
|
||||
```
|
||||
---
|
||||
### `ui_auth`
|
||||
|
||||
The amount of time to allow a user-interactive authentication session to be active.
|
||||
|
||||
This defaults to 0, meaning the user is queried for their credentials
|
||||
before every action, but this can be overridden to allow a single
|
||||
validation to be re-used. This weakens the protections afforded by
|
||||
the user-interactive authentication process, by allowing for multiple
|
||||
(and potentially different) operations to use the same validation session.
|
||||
|
||||
This is ignored for potentially "dangerous" operations (including
|
||||
deactivating an account, modifying an account password, adding a 3PID,
|
||||
and minting additional login tokens).
|
||||
|
||||
Use the `session_timeout` sub-option here to change the time allowed for credential validation.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
ui_auth:
|
||||
session_timeout: "15s"
|
||||
```
|
||||
---
|
||||
### `login_via_existing_session`
|
||||
|
||||
Matrix supports the ability of an existing session to mint a login token for
|
||||
another client.
|
||||
|
||||
Synapse disables this by default as it has security ramifications -- a malicious
|
||||
client could use the mechanism to spawn more than one session.
|
||||
|
||||
The duration of time the generated token is valid for can be configured with the
|
||||
`token_timeout` sub-option.
|
||||
|
||||
User-interactive authentication is required when this is enabled unless the
|
||||
`require_ui_auth` sub-option is set to `False`.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
login_via_existing_session:
|
||||
enabled: true
|
||||
require_ui_auth: false
|
||||
token_timeout: "5m"
|
||||
```
|
||||
---
|
||||
## Metrics
|
||||
Config options related to metrics.
|
||||
|
@ -3415,28 +3484,6 @@ password_config:
|
|||
require_uppercase: true
|
||||
```
|
||||
---
|
||||
### `ui_auth`
|
||||
|
||||
The amount of time to allow a user-interactive authentication session to be active.
|
||||
|
||||
This defaults to 0, meaning the user is queried for their credentials
|
||||
before every action, but this can be overridden to allow a single
|
||||
validation to be re-used. This weakens the protections afforded by
|
||||
the user-interactive authentication process, by allowing for multiple
|
||||
(and potentially different) operations to use the same validation session.
|
||||
|
||||
This is ignored for potentially "dangerous" operations (including
|
||||
deactivating an account, modifying an account password, and
|
||||
adding a 3PID).
|
||||
|
||||
Use the `session_timeout` sub-option here to change the time allowed for credential validation.
|
||||
|
||||
Example configuration:
|
||||
```yaml
|
||||
ui_auth:
|
||||
session_timeout: "15s"
|
||||
```
|
||||
---
|
||||
## Push
|
||||
Configuration settings related to push notifications
|
||||
|
||||
|
|
26
mypy.ini
26
mypy.ini
|
@ -2,17 +2,29 @@
|
|||
namespace_packages = True
|
||||
plugins = pydantic.mypy, mypy_zope:plugin, scripts-dev/mypy_synapse_plugin.py
|
||||
follow_imports = normal
|
||||
check_untyped_defs = True
|
||||
show_error_codes = True
|
||||
show_traceback = True
|
||||
mypy_path = stubs
|
||||
warn_unreachable = True
|
||||
warn_unused_ignores = True
|
||||
local_partial_types = True
|
||||
no_implicit_optional = True
|
||||
|
||||
# Strict checks, see mypy --help
|
||||
warn_unused_configs = True
|
||||
# disallow_any_generics = True
|
||||
disallow_subclassing_any = True
|
||||
# disallow_untyped_calls = True
|
||||
disallow_untyped_defs = True
|
||||
strict_equality = True
|
||||
disallow_incomplete_defs = True
|
||||
# check_untyped_defs = True
|
||||
# disallow_untyped_decorators = True
|
||||
warn_redundant_casts = True
|
||||
warn_unused_ignores = True
|
||||
# warn_return_any = True
|
||||
# no_implicit_reexport = True
|
||||
strict_equality = True
|
||||
strict_concatenate = True
|
||||
|
||||
# Run mypy type checking with the minimum supported Python version to catch new usage
|
||||
# that isn't backwards-compatible (types, overloads, etc).
|
||||
python_version = 3.8
|
||||
|
@ -31,6 +43,7 @@ warn_unused_ignores = False
|
|||
|
||||
[mypy-synapse.util.caches.treecache]
|
||||
disallow_untyped_defs = False
|
||||
disallow_incomplete_defs = False
|
||||
|
||||
;; Dependencies without annotations
|
||||
;; Before ignoring a module, check to see if type stubs are available.
|
||||
|
@ -40,18 +53,18 @@ disallow_untyped_defs = False
|
|||
;; which we can pull in as a dev dependency by adding to `pyproject.toml`'s
|
||||
;; `[tool.poetry.dev-dependencies]` list.
|
||||
|
||||
# https://github.com/lepture/authlib/issues/460
|
||||
[mypy-authlib.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ijson.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-lxml]
|
||||
ignore_missing_imports = True
|
||||
|
||||
# https://github.com/msgpack/msgpack-python/issues/448
|
||||
[mypy-msgpack]
|
||||
ignore_missing_imports = True
|
||||
|
||||
# https://github.com/wolever/parameterized/issues/143
|
||||
[mypy-parameterized.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
@ -73,6 +86,7 @@ ignore_missing_imports = True
|
|||
[mypy-srvlookup.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
# https://github.com/twisted/treq/pull/366
|
||||
[mypy-treq.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -89,7 +89,7 @@ manifest-path = "rust/Cargo.toml"
|
|||
|
||||
[tool.poetry]
|
||||
name = "matrix-synapse"
|
||||
version = "1.85.0rc1"
|
||||
version = "1.85.2"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||
license = "Apache-2.0"
|
||||
|
@ -314,6 +314,7 @@ black = ">=22.3.0"
|
|||
ruff = "0.0.265"
|
||||
|
||||
# Typechecking
|
||||
lxml-stubs = ">=0.4.0"
|
||||
mypy = "*"
|
||||
mypy-zope = "*"
|
||||
types-bleach = ">=4.1.0"
|
||||
|
|
|
@ -7,7 +7,7 @@ name = "synapse"
|
|||
version = "0.1.0"
|
||||
|
||||
edition = "2021"
|
||||
rust-version = "1.58.1"
|
||||
rust-version = "1.60.0"
|
||||
|
||||
[lib]
|
||||
name = "synapse"
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
#![feature(test)]
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use synapse::push::{
|
||||
evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, JsonValue,
|
||||
PushRules, SimpleJsonValue,
|
||||
|
@ -197,7 +195,6 @@ fn bench_eval_message(b: &mut Bencher) {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
b.iter(|| eval.run(&rules, Some("bob"), Some("person")));
|
||||
|
|
|
@ -142,11 +142,11 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
|||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed(".org.matrix.msc3952.is_user_mention"),
|
||||
rule_id: Cow::Borrowed("global/override/.m.is_user_mention"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[Condition::Known(
|
||||
KnownCondition::ExactEventPropertyContainsType(EventPropertyIsTypeCondition {
|
||||
key: Cow::Borrowed("content.org\\.matrix\\.msc3952\\.mentions.user_ids"),
|
||||
key: Cow::Borrowed("content.m\\.mentions.user_ids"),
|
||||
value_type: Cow::Borrowed(&EventMatchPatternType::UserId),
|
||||
}),
|
||||
)]),
|
||||
|
@ -163,11 +163,11 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
|||
default_enabled: true,
|
||||
},
|
||||
PushRule {
|
||||
rule_id: Cow::Borrowed(".org.matrix.msc3952.is_room_mention"),
|
||||
rule_id: Cow::Borrowed("global/override/.m.is_room_mention"),
|
||||
priority_class: 5,
|
||||
conditions: Cow::Borrowed(&[
|
||||
Condition::Known(KnownCondition::EventPropertyIs(EventPropertyIsCondition {
|
||||
key: Cow::Borrowed("content.org\\.matrix\\.msc3952\\.mentions.room"),
|
||||
key: Cow::Borrowed("content.m\\.mentions.room"),
|
||||
value: Cow::Borrowed(&SimpleJsonValue::Bool(true)),
|
||||
})),
|
||||
Condition::Known(KnownCondition::SenderNotificationPermission {
|
||||
|
|
|
@ -70,7 +70,9 @@ pub struct PushRuleEvaluator {
|
|||
/// The "content.body", if any.
|
||||
body: String,
|
||||
|
||||
/// True if the event has a mentions property and MSC3952 support is enabled.
|
||||
/// True if the event has a m.mentions property. (Note that this is a separate
|
||||
/// flag instead of checking flattened_keys since the m.mentions property
|
||||
/// might be an empty map and not appear in flattened_keys.
|
||||
has_mentions: bool,
|
||||
|
||||
/// The number of users in the room.
|
||||
|
@ -155,9 +157,7 @@ impl PushRuleEvaluator {
|
|||
let rule_id = &push_rule.rule_id().to_string();
|
||||
|
||||
// For backwards-compatibility the legacy mention rules are disabled
|
||||
// if the event contains the 'm.mentions' property (and if the
|
||||
// experimental feature is enabled, both of these are represented
|
||||
// by the has_mentions flag).
|
||||
// if the event contains the 'm.mentions' property.
|
||||
if self.has_mentions
|
||||
&& (rule_id == "global/override/.m.rule.contains_display_name"
|
||||
|| rule_id == "global/content/.m.rule.contains_user_name"
|
||||
|
@ -562,7 +562,7 @@ fn test_requires_room_version_supports_condition() {
|
|||
};
|
||||
let rules = PushRules::new(vec![custom_rule]);
|
||||
result = evaluator.run(
|
||||
&FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false, false),
|
||||
&FilteredPushRules::py_new(rules, BTreeMap::new(), true, false, true, false),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
|
|
@ -527,7 +527,6 @@ pub struct FilteredPushRules {
|
|||
msc1767_enabled: bool,
|
||||
msc3381_polls_enabled: bool,
|
||||
msc3664_enabled: bool,
|
||||
msc3952_intentional_mentions: bool,
|
||||
msc3958_suppress_edits_enabled: bool,
|
||||
}
|
||||
|
||||
|
@ -540,7 +539,6 @@ impl FilteredPushRules {
|
|||
msc1767_enabled: bool,
|
||||
msc3381_polls_enabled: bool,
|
||||
msc3664_enabled: bool,
|
||||
msc3952_intentional_mentions: bool,
|
||||
msc3958_suppress_edits_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
@ -549,7 +547,6 @@ impl FilteredPushRules {
|
|||
msc1767_enabled,
|
||||
msc3381_polls_enabled,
|
||||
msc3664_enabled,
|
||||
msc3952_intentional_mentions,
|
||||
msc3958_suppress_edits_enabled,
|
||||
}
|
||||
}
|
||||
|
@ -587,10 +584,6 @@ impl FilteredPushRules {
|
|||
return false;
|
||||
}
|
||||
|
||||
if !self.msc3952_intentional_mentions && rule.rule_id.contains("org.matrix.msc3952")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !self.msc3958_suppress_edits_enabled
|
||||
&& rule.rule_id == "global/override/.com.beeper.suppress_edits"
|
||||
{
|
||||
|
|
|
@ -20,6 +20,8 @@ from concurrent.futures import ThreadPoolExecutor
|
|||
from types import FrameType
|
||||
from typing import Collection, Optional, Sequence, Set
|
||||
|
||||
# These are expanded inside the dockerfile to be a fully qualified image name.
|
||||
# e.g. docker.io/library/debian:bullseye
|
||||
DISTS = (
|
||||
"debian:buster", # oldstable: EOL 2022-08
|
||||
"debian:bullseye",
|
||||
|
|
|
@ -269,6 +269,10 @@ if [[ -n "$SYNAPSE_TEST_LOG_LEVEL" ]]; then
|
|||
export PASS_SYNAPSE_LOG_SENSITIVE=1
|
||||
fi
|
||||
|
||||
# Log a few more useful things for a developer attempting to debug something
|
||||
# particularly tricky.
|
||||
export PASS_SYNAPSE_LOG_TESTING=1
|
||||
|
||||
# Run the tests!
|
||||
echo "Images built; running complement"
|
||||
cd "$COMPLEMENT_DIR"
|
||||
|
|
|
@ -46,7 +46,6 @@ class FilteredPushRules:
|
|||
msc1767_enabled: bool,
|
||||
msc3381_polls_enabled: bool,
|
||||
msc3664_enabled: bool,
|
||||
msc3952_intentional_mentions: bool,
|
||||
msc3958_suppress_edits_enabled: bool,
|
||||
): ...
|
||||
def rules(self) -> Collection[Tuple[PushRule, bool]]: ...
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
# Copyright 2023 The Matrix.org Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.types import Requester
|
||||
|
||||
# guests always get this device id.
|
||||
GUEST_DEVICE_ID = "guest_device"
|
||||
|
||||
|
||||
class Auth(Protocol):
|
||||
"""The interface that an auth provider must implement."""
|
||||
|
||||
async def check_user_in_room(
|
||||
self,
|
||||
room_id: str,
|
||||
requester: Requester,
|
||||
allow_departed_users: bool = False,
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""Check if the user is in the room, or was at some point.
|
||||
Args:
|
||||
room_id: The room to check.
|
||||
|
||||
user_id: The user to check.
|
||||
|
||||
current_state: Optional map of the current state of the room.
|
||||
If provided then that map is used to check whether they are a
|
||||
member of the room. Otherwise the current membership is
|
||||
loaded from the database.
|
||||
|
||||
allow_departed_users: if True, accept users that were previously
|
||||
members but have now departed.
|
||||
|
||||
Raises:
|
||||
AuthError if the user is/was not in the room.
|
||||
Returns:
|
||||
The current membership of the user in the room and the
|
||||
membership event ID of the user.
|
||||
"""
|
||||
|
||||
async def get_user_by_req(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
allow_guest: bool = False,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
"""Get a registered user's ID.
|
||||
|
||||
Args:
|
||||
request: An HTTP request with an access_token query parameter.
|
||||
allow_guest: If False, will raise an AuthError if the user making the
|
||||
request is a guest.
|
||||
allow_expired: If True, allow the request through even if the account
|
||||
is expired, or session token lifetime has ended. Note that
|
||||
/login will deliver access tokens regardless of expiration.
|
||||
|
||||
Returns:
|
||||
Resolves to the requester
|
||||
Raises:
|
||||
InvalidClientCredentialsError if no user by that token exists or the token
|
||||
is invalid.
|
||||
AuthError if access is denied for the user in the access token
|
||||
"""
|
||||
|
||||
async def validate_appservice_can_control_user_id(
|
||||
self, app_service: ApplicationService, user_id: str
|
||||
) -> None:
|
||||
"""Validates that the app service is allowed to control
|
||||
the given user.
|
||||
|
||||
Args:
|
||||
app_service: The app service that controls the user
|
||||
user_id: The author MXID that the app service is controlling
|
||||
|
||||
Raises:
|
||||
AuthError: If the application service is not allowed to control the user
|
||||
(user namespace regex does not match, wrong homeserver, etc)
|
||||
or if the user has not been registered yet.
|
||||
"""
|
||||
|
||||
async def get_user_by_access_token(
|
||||
self,
|
||||
token: str,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
"""Validate access token and get user_id from it
|
||||
|
||||
Args:
|
||||
token: The access token to get the user by
|
||||
allow_expired: If False, raises an InvalidClientTokenError
|
||||
if the token is expired
|
||||
|
||||
Raises:
|
||||
InvalidClientTokenError if a user by that token exists, but the token is
|
||||
expired
|
||||
InvalidClientCredentialsError if no user by that token exists or the token
|
||||
is invalid
|
||||
"""
|
||||
|
||||
async def is_server_admin(self, requester: Requester) -> bool:
|
||||
"""Check if the given user is a local server admin.
|
||||
|
||||
Args:
|
||||
requester: user to check
|
||||
|
||||
Returns:
|
||||
True if the user is an admin
|
||||
"""
|
||||
|
||||
async def check_can_change_room_list(
|
||||
self, room_id: str, requester: Requester
|
||||
) -> bool:
|
||||
"""Determine whether the user is allowed to edit the room's entry in the
|
||||
published room list.
|
||||
|
||||
Args:
|
||||
room_id
|
||||
user
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def has_access_token(request: Request) -> bool:
|
||||
"""Checks if the request has an access_token.
|
||||
|
||||
Returns:
|
||||
False if no access_token was given, True otherwise.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_access_token_from_request(request: Request) -> str:
|
||||
"""Extracts the access_token from the request.
|
||||
|
||||
Args:
|
||||
request: The http request.
|
||||
Returns:
|
||||
The access_token
|
||||
Raises:
|
||||
MissingClientTokenError: If there isn't a single access_token in the
|
||||
request
|
||||
"""
|
||||
|
||||
async def check_user_in_room_or_world_readable(
|
||||
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""Checks that the user is or was in the room or the room is world
|
||||
readable. If it isn't then an exception is raised.
|
||||
|
||||
Args:
|
||||
room_id: room to check
|
||||
user_id: user to check
|
||||
allow_departed_users: if True, accept users that were previously
|
||||
members but have now departed
|
||||
|
||||
Returns:
|
||||
Resolves to the current membership of the user in the room and the
|
||||
membership event ID of the user. If the user is not in the room and
|
||||
never has been, then `(Membership.JOIN, None)` is returned.
|
||||
"""
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||
# Copyright 2023 The Matrix.org Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,6 @@
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
import pymacaroons
|
||||
from netaddr import IPAddress
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
@ -24,19 +23,11 @@ from synapse.api.constants import EventTypes, HistoryVisibility, Membership
|
|||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
InvalidClientTokenError,
|
||||
MissingClientTokenError,
|
||||
UnstableSpecAuthError,
|
||||
)
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.http import get_request_user_agent
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.opentracing import (
|
||||
active_span,
|
||||
force_tracing,
|
||||
start_active_span,
|
||||
trace,
|
||||
)
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.types import Requester, create_requester
|
||||
from synapse.util.cancellation import cancellable
|
||||
|
||||
|
@ -46,26 +37,13 @@ if TYPE_CHECKING:
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# guests always get this device id.
|
||||
GUEST_DEVICE_ID = "guest_device"
|
||||
|
||||
|
||||
class Auth:
|
||||
"""
|
||||
This class contains functions for authenticating users of our client-server API.
|
||||
"""
|
||||
class BaseAuth:
|
||||
"""Common base class for all auth implementations."""
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.clock = hs.get_clock()
|
||||
self.store = hs.get_datastores().main
|
||||
self._account_validity_handler = hs.get_account_validity_handler()
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._macaroon_generator = hs.get_macaroon_generator()
|
||||
|
||||
self._track_appservice_user_ips = hs.config.appservice.track_appservice_user_ips
|
||||
self._track_puppeted_user_ips = hs.config.api.track_puppeted_user_ips
|
||||
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
|
||||
|
||||
async def check_user_in_room(
|
||||
self,
|
||||
|
@ -119,139 +97,49 @@ class Auth:
|
|||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
@cancellable
|
||||
async def get_user_by_req(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
allow_guest: bool = False,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
"""Get a registered user's ID.
|
||||
@trace
|
||||
async def check_user_in_room_or_world_readable(
|
||||
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""Checks that the user is or was in the room or the room is world
|
||||
readable. If it isn't then an exception is raised.
|
||||
|
||||
Args:
|
||||
request: An HTTP request with an access_token query parameter.
|
||||
allow_guest: If False, will raise an AuthError if the user making the
|
||||
request is a guest.
|
||||
allow_expired: If True, allow the request through even if the account
|
||||
is expired, or session token lifetime has ended. Note that
|
||||
/login will deliver access tokens regardless of expiration.
|
||||
room_id: room to check
|
||||
user_id: user to check
|
||||
allow_departed_users: if True, accept users that were previously
|
||||
members but have now departed
|
||||
|
||||
Returns:
|
||||
Resolves to the requester
|
||||
Raises:
|
||||
InvalidClientCredentialsError if no user by that token exists or the token
|
||||
is invalid.
|
||||
AuthError if access is denied for the user in the access token
|
||||
Resolves to the current membership of the user in the room and the
|
||||
membership event ID of the user. If the user is not in the room and
|
||||
never has been, then `(Membership.JOIN, None)` is returned.
|
||||
"""
|
||||
parent_span = active_span()
|
||||
with start_active_span("get_user_by_req"):
|
||||
requester = await self._wrapped_get_user_by_req(
|
||||
request, allow_guest, allow_expired
|
||||
)
|
||||
|
||||
if parent_span:
|
||||
if requester.authenticated_entity in self._force_tracing_for_users:
|
||||
# request tracing is enabled for this user, so we need to force it
|
||||
# tracing on for the parent span (which will be the servlet span).
|
||||
#
|
||||
# It's too late for the get_user_by_req span to inherit the setting,
|
||||
# so we also force it on for that.
|
||||
force_tracing()
|
||||
force_tracing(parent_span)
|
||||
parent_span.set_tag(
|
||||
"authenticated_entity", requester.authenticated_entity
|
||||
)
|
||||
parent_span.set_tag("user_id", requester.user.to_string())
|
||||
if requester.device_id is not None:
|
||||
parent_span.set_tag("device_id", requester.device_id)
|
||||
if requester.app_service is not None:
|
||||
parent_span.set_tag("appservice_id", requester.app_service.id)
|
||||
return requester
|
||||
|
||||
@cancellable
|
||||
async def _wrapped_get_user_by_req(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
allow_guest: bool,
|
||||
allow_expired: bool,
|
||||
) -> Requester:
|
||||
"""Helper for get_user_by_req
|
||||
|
||||
Once get_user_by_req has set up the opentracing span, this does the actual work.
|
||||
"""
|
||||
try:
|
||||
ip_addr = request.getClientAddress().host
|
||||
user_agent = get_request_user_agent(request)
|
||||
|
||||
access_token = self.get_access_token_from_request(request)
|
||||
|
||||
# First check if it could be a request from an appservice
|
||||
requester = await self._get_appservice_user(request)
|
||||
if not requester:
|
||||
# If not, it should be from a regular user
|
||||
requester = await self.get_user_by_access_token(
|
||||
access_token, allow_expired=allow_expired
|
||||
)
|
||||
|
||||
# Deny the request if the user account has expired.
|
||||
# This check is only done for regular users, not appservice ones.
|
||||
if not allow_expired:
|
||||
if await self._account_validity_handler.is_user_expired(
|
||||
requester.user.to_string()
|
||||
):
|
||||
# Raise the error if either an account validity module has determined
|
||||
# the account has expired, or the legacy account validity
|
||||
# implementation is enabled and determined the account has expired
|
||||
raise AuthError(
|
||||
403,
|
||||
"User account has expired",
|
||||
errcode=Codes.EXPIRED_ACCOUNT,
|
||||
)
|
||||
|
||||
if ip_addr and (
|
||||
not requester.app_service or self._track_appservice_user_ips
|
||||
# check_user_in_room will return the most recent membership
|
||||
# event for the user if:
|
||||
# * The user is a non-guest user, and was ever in the room
|
||||
# * The user is a guest user, and has joined the room
|
||||
# else it will throw.
|
||||
return await self.check_user_in_room(
|
||||
room_id, requester, allow_departed_users=allow_departed_users
|
||||
)
|
||||
except AuthError:
|
||||
visibility = await self._storage_controllers.state.get_current_state_event(
|
||||
room_id, EventTypes.RoomHistoryVisibility, ""
|
||||
)
|
||||
if (
|
||||
visibility
|
||||
and visibility.content.get("history_visibility")
|
||||
== HistoryVisibility.WORLD_READABLE
|
||||
):
|
||||
# XXX(quenting): I'm 95% confident that we could skip setting the
|
||||
# device_id to "dummy-device" for appservices, and that the only impact
|
||||
# would be some rows which whould not deduplicate in the 'user_ips'
|
||||
# table during the transition
|
||||
recorded_device_id = (
|
||||
"dummy-device"
|
||||
if requester.device_id is None and requester.app_service is not None
|
||||
else requester.device_id
|
||||
)
|
||||
await self.store.insert_client_ip(
|
||||
user_id=requester.authenticated_entity,
|
||||
access_token=access_token,
|
||||
ip=ip_addr,
|
||||
user_agent=user_agent,
|
||||
device_id=recorded_device_id,
|
||||
)
|
||||
|
||||
# Track also the puppeted user client IP if enabled and the user is puppeting
|
||||
if (
|
||||
requester.user.to_string() != requester.authenticated_entity
|
||||
and self._track_puppeted_user_ips
|
||||
):
|
||||
await self.store.insert_client_ip(
|
||||
user_id=requester.user.to_string(),
|
||||
access_token=access_token,
|
||||
ip=ip_addr,
|
||||
user_agent=user_agent,
|
||||
device_id=requester.device_id,
|
||||
)
|
||||
|
||||
if requester.is_guest and not allow_guest:
|
||||
raise AuthError(
|
||||
403,
|
||||
"Guest access not allowed",
|
||||
errcode=Codes.GUEST_ACCESS_FORBIDDEN,
|
||||
)
|
||||
|
||||
request.requester = requester
|
||||
return requester
|
||||
except KeyError:
|
||||
raise MissingClientTokenError()
|
||||
return Membership.JOIN, None
|
||||
raise AuthError(
|
||||
403,
|
||||
"User %r not in room %s, and room previews are disabled"
|
||||
% (requester.user, room_id),
|
||||
)
|
||||
|
||||
async def validate_appservice_can_control_user_id(
|
||||
self, app_service: ApplicationService, user_id: str
|
||||
|
@ -284,184 +172,16 @@ class Auth:
|
|||
403, "Application service has not registered this user (%s)" % user_id
|
||||
)
|
||||
|
||||
@cancellable
|
||||
async def _get_appservice_user(self, request: Request) -> Optional[Requester]:
|
||||
"""
|
||||
Given a request, reads the request parameters to determine:
|
||||
- whether it's an application service that's making this request
|
||||
- what user the application service should be treated as controlling
|
||||
(the user_id URI parameter allows an application service to masquerade
|
||||
any applicable user in its namespace)
|
||||
- what device the application service should be treated as controlling
|
||||
(the device_id[^1] URI parameter allows an application service to masquerade
|
||||
as any device that exists for the relevant user)
|
||||
|
||||
[^1] Unstable and provided by MSC3202.
|
||||
Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
|
||||
|
||||
Returns:
|
||||
the application service `Requester` of that request
|
||||
|
||||
Postconditions:
|
||||
- The `app_service` field in the returned `Requester` is set
|
||||
- The `user_id` field in the returned `Requester` is either the application
|
||||
service sender or the controlled user set by the `user_id` URI parameter
|
||||
- The returned application service is permitted to control the returned user ID.
|
||||
- The returned device ID, if present, has been checked to be a valid device ID
|
||||
for the returned user ID.
|
||||
"""
|
||||
DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
|
||||
|
||||
app_service = self.store.get_app_service_by_token(
|
||||
self.get_access_token_from_request(request)
|
||||
)
|
||||
if app_service is None:
|
||||
return None
|
||||
|
||||
if app_service.ip_range_whitelist:
|
||||
ip_address = IPAddress(request.getClientAddress().host)
|
||||
if ip_address not in app_service.ip_range_whitelist:
|
||||
return None
|
||||
|
||||
# This will always be set by the time Twisted calls us.
|
||||
assert request.args is not None
|
||||
|
||||
if b"user_id" in request.args:
|
||||
effective_user_id = request.args[b"user_id"][0].decode("utf8")
|
||||
await self.validate_appservice_can_control_user_id(
|
||||
app_service, effective_user_id
|
||||
)
|
||||
else:
|
||||
effective_user_id = app_service.sender
|
||||
|
||||
effective_device_id: Optional[str] = None
|
||||
|
||||
if (
|
||||
self.hs.config.experimental.msc3202_device_masquerading_enabled
|
||||
and DEVICE_ID_ARG_NAME in request.args
|
||||
):
|
||||
effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
|
||||
# We only just set this so it can't be None!
|
||||
assert effective_device_id is not None
|
||||
device_opt = await self.store.get_device(
|
||||
effective_user_id, effective_device_id
|
||||
)
|
||||
if device_opt is None:
|
||||
# For now, use 400 M_EXCLUSIVE if the device doesn't exist.
|
||||
# This is an open thread of discussion on MSC3202 as of 2021-12-09.
|
||||
raise AuthError(
|
||||
400,
|
||||
f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
|
||||
Codes.EXCLUSIVE,
|
||||
)
|
||||
|
||||
return create_requester(
|
||||
effective_user_id, app_service=app_service, device_id=effective_device_id
|
||||
)
|
||||
|
||||
async def get_user_by_access_token(
|
||||
self,
|
||||
token: str,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
"""Validate access token and get user_id from it
|
||||
|
||||
Args:
|
||||
token: The access token to get the user by
|
||||
allow_expired: If False, raises an InvalidClientTokenError
|
||||
if the token is expired
|
||||
|
||||
Raises:
|
||||
InvalidClientTokenError if a user by that token exists, but the token is
|
||||
expired
|
||||
InvalidClientCredentialsError if no user by that token exists or the token
|
||||
is invalid
|
||||
"""
|
||||
|
||||
# First look in the database to see if the access token is present
|
||||
# as an opaque token.
|
||||
user_info = await self.store.get_user_by_access_token(token)
|
||||
if user_info:
|
||||
valid_until_ms = user_info.valid_until_ms
|
||||
if (
|
||||
not allow_expired
|
||||
and valid_until_ms is not None
|
||||
and valid_until_ms < self.clock.time_msec()
|
||||
):
|
||||
# there was a valid access token, but it has expired.
|
||||
# soft-logout the user.
|
||||
raise InvalidClientTokenError(
|
||||
msg="Access token has expired", soft_logout=True
|
||||
)
|
||||
|
||||
# Mark the token as used. This is used to invalidate old refresh
|
||||
# tokens after some time.
|
||||
await self.store.mark_access_token_as_used(user_info.token_id)
|
||||
|
||||
requester = create_requester(
|
||||
user_id=user_info.user_id,
|
||||
access_token_id=user_info.token_id,
|
||||
is_guest=user_info.is_guest,
|
||||
shadow_banned=user_info.shadow_banned,
|
||||
device_id=user_info.device_id,
|
||||
authenticated_entity=user_info.token_owner,
|
||||
)
|
||||
|
||||
return requester
|
||||
|
||||
# If the token isn't found in the database, then it could still be a
|
||||
# macaroon for a guest, so we check that here.
|
||||
try:
|
||||
user_id = self._macaroon_generator.verify_guest_token(token)
|
||||
|
||||
# Guest access tokens are not stored in the database (there can
|
||||
# only be one access token per guest, anyway).
|
||||
#
|
||||
# In order to prevent guest access tokens being used as regular
|
||||
# user access tokens (and hence getting around the invalidation
|
||||
# process), we look up the user id and check that it is indeed
|
||||
# a guest user.
|
||||
#
|
||||
# It would of course be much easier to store guest access
|
||||
# tokens in the database as well, but that would break existing
|
||||
# guest tokens.
|
||||
stored_user = await self.store.get_user_by_id(user_id)
|
||||
if not stored_user:
|
||||
raise InvalidClientTokenError("Unknown user_id %s" % user_id)
|
||||
if not stored_user["is_guest"]:
|
||||
raise InvalidClientTokenError(
|
||||
"Guest access token used for regular user"
|
||||
)
|
||||
|
||||
return create_requester(
|
||||
user_id=user_id,
|
||||
is_guest=True,
|
||||
# all guests get the same device id
|
||||
device_id=GUEST_DEVICE_ID,
|
||||
authenticated_entity=user_id,
|
||||
)
|
||||
except (
|
||||
pymacaroons.exceptions.MacaroonException,
|
||||
TypeError,
|
||||
ValueError,
|
||||
) as e:
|
||||
logger.warning(
|
||||
"Invalid access token in auth: %s %s.",
|
||||
type(e),
|
||||
e,
|
||||
)
|
||||
raise InvalidClientTokenError("Invalid access token passed.")
|
||||
|
||||
async def is_server_admin(self, requester: Requester) -> bool:
|
||||
"""Check if the given user is a local server admin.
|
||||
|
||||
Args:
|
||||
requester: The user making the request, according to the access token.
|
||||
requester: user to check
|
||||
|
||||
Returns:
|
||||
True if the user is an admin
|
||||
"""
|
||||
return await self.store.is_server_admin(requester.user)
|
||||
raise NotImplementedError()
|
||||
|
||||
async def check_can_change_room_list(
|
||||
self, room_id: str, requester: Requester
|
||||
|
@ -470,8 +190,8 @@ class Auth:
|
|||
published room list.
|
||||
|
||||
Args:
|
||||
room_id: The room to check.
|
||||
requester: The user making the request, according to the access token.
|
||||
room_id
|
||||
user
|
||||
"""
|
||||
|
||||
is_admin = await self.is_server_admin(requester)
|
||||
|
@ -518,7 +238,6 @@ class Auth:
|
|||
return bool(query_params) or bool(auth_headers)
|
||||
|
||||
@staticmethod
|
||||
@cancellable
|
||||
def get_access_token_from_request(request: Request) -> str:
|
||||
"""Extracts the access_token from the request.
|
||||
|
||||
|
@ -556,47 +275,77 @@ class Auth:
|
|||
|
||||
return query_params[0].decode("ascii")
|
||||
|
||||
@trace
|
||||
async def check_user_in_room_or_world_readable(
|
||||
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
"""Checks that the user is or was in the room or the room is world
|
||||
readable. If it isn't then an exception is raised.
|
||||
@cancellable
|
||||
async def get_appservice_user(
|
||||
self, request: Request, access_token: str
|
||||
) -> Optional[Requester]:
|
||||
"""
|
||||
Given a request, reads the request parameters to determine:
|
||||
- whether it's an application service that's making this request
|
||||
- what user the application service should be treated as controlling
|
||||
(the user_id URI parameter allows an application service to masquerade
|
||||
any applicable user in its namespace)
|
||||
- what device the application service should be treated as controlling
|
||||
(the device_id[^1] URI parameter allows an application service to masquerade
|
||||
as any device that exists for the relevant user)
|
||||
|
||||
Args:
|
||||
room_id: The room to check.
|
||||
requester: The user making the request, according to the access token.
|
||||
allow_departed_users: If True, accept users that were previously
|
||||
members but have now departed.
|
||||
[^1] Unstable and provided by MSC3202.
|
||||
Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
|
||||
|
||||
Returns:
|
||||
Resolves to the current membership of the user in the room and the
|
||||
membership event ID of the user. If the user is not in the room and
|
||||
never has been, then `(Membership.JOIN, None)` is returned.
|
||||
"""
|
||||
the application service `Requester` of that request
|
||||
|
||||
try:
|
||||
# check_user_in_room will return the most recent membership
|
||||
# event for the user if:
|
||||
# * The user is a non-guest user, and was ever in the room
|
||||
# * The user is a guest user, and has joined the room
|
||||
# else it will throw.
|
||||
return await self.check_user_in_room(
|
||||
room_id, requester, allow_departed_users=allow_departed_users
|
||||
Postconditions:
|
||||
- The `app_service` field in the returned `Requester` is set
|
||||
- The `user_id` field in the returned `Requester` is either the application
|
||||
service sender or the controlled user set by the `user_id` URI parameter
|
||||
- The returned application service is permitted to control the returned user ID.
|
||||
- The returned device ID, if present, has been checked to be a valid device ID
|
||||
for the returned user ID.
|
||||
"""
|
||||
DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
|
||||
|
||||
app_service = self.store.get_app_service_by_token(access_token)
|
||||
if app_service is None:
|
||||
return None
|
||||
|
||||
if app_service.ip_range_whitelist:
|
||||
ip_address = IPAddress(request.getClientAddress().host)
|
||||
if ip_address not in app_service.ip_range_whitelist:
|
||||
return None
|
||||
|
||||
# This will always be set by the time Twisted calls us.
|
||||
assert request.args is not None
|
||||
|
||||
if b"user_id" in request.args:
|
||||
effective_user_id = request.args[b"user_id"][0].decode("utf8")
|
||||
await self.validate_appservice_can_control_user_id(
|
||||
app_service, effective_user_id
|
||||
)
|
||||
except AuthError:
|
||||
visibility = await self._storage_controllers.state.get_current_state_event(
|
||||
room_id, EventTypes.RoomHistoryVisibility, ""
|
||||
)
|
||||
if (
|
||||
visibility
|
||||
and visibility.content.get("history_visibility")
|
||||
== HistoryVisibility.WORLD_READABLE
|
||||
):
|
||||
return Membership.JOIN, None
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s, and room previews are disabled"
|
||||
% (requester.user, room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
else:
|
||||
effective_user_id = app_service.sender
|
||||
|
||||
effective_device_id: Optional[str] = None
|
||||
|
||||
if (
|
||||
self.hs.config.experimental.msc3202_device_masquerading_enabled
|
||||
and DEVICE_ID_ARG_NAME in request.args
|
||||
):
|
||||
effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
|
||||
# We only just set this so it can't be None!
|
||||
assert effective_device_id is not None
|
||||
device_opt = await self.store.get_device(
|
||||
effective_user_id, effective_device_id
|
||||
)
|
||||
if device_opt is None:
|
||||
# For now, use 400 M_EXCLUSIVE if the device doesn't exist.
|
||||
# This is an open thread of discussion on MSC3202 as of 2021-12-09.
|
||||
raise AuthError(
|
||||
400,
|
||||
f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
|
||||
Codes.EXCLUSIVE,
|
||||
)
|
||||
|
||||
return create_requester(
|
||||
effective_user_id, app_service=app_service, device_id=effective_device_id
|
||||
)
|
|
@ -0,0 +1,291 @@
|
|||
# Copyright 2023 The Matrix.org Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pymacaroons
|
||||
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
InvalidClientTokenError,
|
||||
MissingClientTokenError,
|
||||
)
|
||||
from synapse.http import get_request_user_agent
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.opentracing import active_span, force_tracing, start_active_span
|
||||
from synapse.types import Requester, create_requester
|
||||
from synapse.util.cancellation import cancellable
|
||||
|
||||
from . import GUEST_DEVICE_ID
|
||||
from .base import BaseAuth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InternalAuth(BaseAuth):
|
||||
"""
|
||||
This class contains functions for authenticating users of our client-server API.
|
||||
"""
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.clock = hs.get_clock()
|
||||
self._account_validity_handler = hs.get_account_validity_handler()
|
||||
self._macaroon_generator = hs.get_macaroon_generator()
|
||||
|
||||
self._track_appservice_user_ips = hs.config.appservice.track_appservice_user_ips
|
||||
self._track_puppeted_user_ips = hs.config.api.track_puppeted_user_ips
|
||||
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
|
||||
|
||||
@cancellable
|
||||
async def get_user_by_req(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
allow_guest: bool = False,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
"""Get a registered user's ID.
|
||||
|
||||
Args:
|
||||
request: An HTTP request with an access_token query parameter.
|
||||
allow_guest: If False, will raise an AuthError if the user making the
|
||||
request is a guest.
|
||||
allow_expired: If True, allow the request through even if the account
|
||||
is expired, or session token lifetime has ended. Note that
|
||||
/login will deliver access tokens regardless of expiration.
|
||||
|
||||
Returns:
|
||||
Resolves to the requester
|
||||
Raises:
|
||||
InvalidClientCredentialsError if no user by that token exists or the token
|
||||
is invalid.
|
||||
AuthError if access is denied for the user in the access token
|
||||
"""
|
||||
parent_span = active_span()
|
||||
with start_active_span("get_user_by_req"):
|
||||
requester = await self._wrapped_get_user_by_req(
|
||||
request, allow_guest, allow_expired
|
||||
)
|
||||
|
||||
if parent_span:
|
||||
if requester.authenticated_entity in self._force_tracing_for_users:
|
||||
# request tracing is enabled for this user, so we need to force it
|
||||
# tracing on for the parent span (which will be the servlet span).
|
||||
#
|
||||
# It's too late for the get_user_by_req span to inherit the setting,
|
||||
# so we also force it on for that.
|
||||
force_tracing()
|
||||
force_tracing(parent_span)
|
||||
parent_span.set_tag(
|
||||
"authenticated_entity", requester.authenticated_entity
|
||||
)
|
||||
parent_span.set_tag("user_id", requester.user.to_string())
|
||||
if requester.device_id is not None:
|
||||
parent_span.set_tag("device_id", requester.device_id)
|
||||
if requester.app_service is not None:
|
||||
parent_span.set_tag("appservice_id", requester.app_service.id)
|
||||
return requester
|
||||
|
||||
@cancellable
|
||||
async def _wrapped_get_user_by_req(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
allow_guest: bool,
|
||||
allow_expired: bool,
|
||||
) -> Requester:
|
||||
"""Helper for get_user_by_req
|
||||
|
||||
Once get_user_by_req has set up the opentracing span, this does the actual work.
|
||||
"""
|
||||
try:
|
||||
ip_addr = request.getClientAddress().host
|
||||
user_agent = get_request_user_agent(request)
|
||||
|
||||
access_token = self.get_access_token_from_request(request)
|
||||
|
||||
# First check if it could be a request from an appservice
|
||||
requester = await self.get_appservice_user(request, access_token)
|
||||
if not requester:
|
||||
# If not, it should be from a regular user
|
||||
requester = await self.get_user_by_access_token(
|
||||
access_token, allow_expired=allow_expired
|
||||
)
|
||||
|
||||
# Deny the request if the user account has expired.
|
||||
# This check is only done for regular users, not appservice ones.
|
||||
if not allow_expired:
|
||||
if await self._account_validity_handler.is_user_expired(
|
||||
requester.user.to_string()
|
||||
):
|
||||
# Raise the error if either an account validity module has determined
|
||||
# the account has expired, or the legacy account validity
|
||||
# implementation is enabled and determined the account has expired
|
||||
raise AuthError(
|
||||
403,
|
||||
"User account has expired",
|
||||
errcode=Codes.EXPIRED_ACCOUNT,
|
||||
)
|
||||
|
||||
if ip_addr and (
|
||||
not requester.app_service or self._track_appservice_user_ips
|
||||
):
|
||||
# XXX(quenting): I'm 95% confident that we could skip setting the
|
||||
# device_id to "dummy-device" for appservices, and that the only impact
|
||||
# would be some rows which whould not deduplicate in the 'user_ips'
|
||||
# table during the transition
|
||||
recorded_device_id = (
|
||||
"dummy-device"
|
||||
if requester.device_id is None and requester.app_service is not None
|
||||
else requester.device_id
|
||||
)
|
||||
await self.store.insert_client_ip(
|
||||
user_id=requester.authenticated_entity,
|
||||
access_token=access_token,
|
||||
ip=ip_addr,
|
||||
user_agent=user_agent,
|
||||
device_id=recorded_device_id,
|
||||
)
|
||||
|
||||
# Track also the puppeted user client IP if enabled and the user is puppeting
|
||||
if (
|
||||
requester.user.to_string() != requester.authenticated_entity
|
||||
and self._track_puppeted_user_ips
|
||||
):
|
||||
await self.store.insert_client_ip(
|
||||
user_id=requester.user.to_string(),
|
||||
access_token=access_token,
|
||||
ip=ip_addr,
|
||||
user_agent=user_agent,
|
||||
device_id=requester.device_id,
|
||||
)
|
||||
|
||||
if requester.is_guest and not allow_guest:
|
||||
raise AuthError(
|
||||
403,
|
||||
"Guest access not allowed",
|
||||
errcode=Codes.GUEST_ACCESS_FORBIDDEN,
|
||||
)
|
||||
|
||||
request.requester = requester
|
||||
return requester
|
||||
except KeyError:
|
||||
raise MissingClientTokenError()
|
||||
|
||||
async def get_user_by_access_token(
|
||||
self,
|
||||
token: str,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
"""Validate access token and get user_id from it
|
||||
|
||||
Args:
|
||||
token: The access token to get the user by
|
||||
allow_expired: If False, raises an InvalidClientTokenError
|
||||
if the token is expired
|
||||
|
||||
Raises:
|
||||
InvalidClientTokenError if a user by that token exists, but the token is
|
||||
expired
|
||||
InvalidClientCredentialsError if no user by that token exists or the token
|
||||
is invalid
|
||||
"""
|
||||
|
||||
# First look in the database to see if the access token is present
|
||||
# as an opaque token.
|
||||
user_info = await self.store.get_user_by_access_token(token)
|
||||
if user_info:
|
||||
valid_until_ms = user_info.valid_until_ms
|
||||
if (
|
||||
not allow_expired
|
||||
and valid_until_ms is not None
|
||||
and valid_until_ms < self.clock.time_msec()
|
||||
):
|
||||
# there was a valid access token, but it has expired.
|
||||
# soft-logout the user.
|
||||
raise InvalidClientTokenError(
|
||||
msg="Access token has expired", soft_logout=True
|
||||
)
|
||||
|
||||
# Mark the token as used. This is used to invalidate old refresh
|
||||
# tokens after some time.
|
||||
await self.store.mark_access_token_as_used(user_info.token_id)
|
||||
|
||||
requester = create_requester(
|
||||
user_id=user_info.user_id,
|
||||
access_token_id=user_info.token_id,
|
||||
is_guest=user_info.is_guest,
|
||||
shadow_banned=user_info.shadow_banned,
|
||||
device_id=user_info.device_id,
|
||||
authenticated_entity=user_info.token_owner,
|
||||
)
|
||||
|
||||
return requester
|
||||
|
||||
# If the token isn't found in the database, then it could still be a
|
||||
# macaroon for a guest, so we check that here.
|
||||
try:
|
||||
user_id = self._macaroon_generator.verify_guest_token(token)
|
||||
|
||||
# Guest access tokens are not stored in the database (there can
|
||||
# only be one access token per guest, anyway).
|
||||
#
|
||||
# In order to prevent guest access tokens being used as regular
|
||||
# user access tokens (and hence getting around the invalidation
|
||||
# process), we look up the user id and check that it is indeed
|
||||
# a guest user.
|
||||
#
|
||||
# It would of course be much easier to store guest access
|
||||
# tokens in the database as well, but that would break existing
|
||||
# guest tokens.
|
||||
stored_user = await self.store.get_user_by_id(user_id)
|
||||
if not stored_user:
|
||||
raise InvalidClientTokenError("Unknown user_id %s" % user_id)
|
||||
if not stored_user["is_guest"]:
|
||||
raise InvalidClientTokenError(
|
||||
"Guest access token used for regular user"
|
||||
)
|
||||
|
||||
return create_requester(
|
||||
user_id=user_id,
|
||||
is_guest=True,
|
||||
# all guests get the same device id
|
||||
device_id=GUEST_DEVICE_ID,
|
||||
authenticated_entity=user_id,
|
||||
)
|
||||
except (
|
||||
pymacaroons.exceptions.MacaroonException,
|
||||
TypeError,
|
||||
ValueError,
|
||||
) as e:
|
||||
logger.warning(
|
||||
"Invalid access token in auth: %s %s.",
|
||||
type(e),
|
||||
e,
|
||||
)
|
||||
raise InvalidClientTokenError("Invalid access token passed.")
|
||||
|
||||
async def is_server_admin(self, requester: Requester) -> bool:
|
||||
"""Check if the given user is a local server admin.
|
||||
|
||||
Args:
|
||||
requester: The user making the request, according to the access token.
|
||||
|
||||
Returns:
|
||||
True if the user is an admin
|
||||
"""
|
||||
return await self.store.is_server_admin(requester.user)
|
|
@ -0,0 +1,352 @@
|
|||
# Copyright 2023 The Matrix.org Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from authlib.oauth2 import ClientAuth
|
||||
from authlib.oauth2.auth import encode_client_secret_basic, encode_client_secret_post
|
||||
from authlib.oauth2.rfc7523 import ClientSecretJWT, PrivateKeyJWT, private_key_jwt_sign
|
||||
from authlib.oauth2.rfc7662 import IntrospectionToken
|
||||
from authlib.oidc.discovery import OpenIDProviderMetadata, get_well_known_url
|
||||
|
||||
from twisted.web.client import readBody
|
||||
from twisted.web.http_headers import Headers
|
||||
|
||||
from synapse.api.auth.base import BaseAuth
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
HttpResponseException,
|
||||
InvalidClientTokenError,
|
||||
OAuthInsufficientScopeError,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.context import make_deferred_yieldable
|
||||
from synapse.types import Requester, UserID, create_requester
|
||||
from synapse.util import json_decoder
|
||||
from synapse.util.caches.cached_call import RetryOnExceptionCachedCall
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Scope as defined by MSC2967
|
||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/2967
|
||||
SCOPE_MATRIX_API = "urn:matrix:org.matrix.msc2967.client:api:*"
|
||||
SCOPE_MATRIX_GUEST = "urn:matrix:org.matrix.msc2967.client:api:guest"
|
||||
SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
|
||||
|
||||
# Scope which allows access to the Synapse admin API
|
||||
SCOPE_SYNAPSE_ADMIN = "urn:synapse:admin:*"
|
||||
|
||||
|
||||
def scope_to_list(scope: str) -> List[str]:
|
||||
"""Convert a scope string to a list of scope tokens"""
|
||||
return scope.strip().split(" ")
|
||||
|
||||
|
||||
class PrivateKeyJWTWithKid(PrivateKeyJWT): # type: ignore[misc]
|
||||
"""An implementation of the private_key_jwt client auth method that includes a kid header.
|
||||
|
||||
This is needed because some providers (Keycloak) require the kid header to figure
|
||||
out which key to use to verify the signature.
|
||||
"""
|
||||
|
||||
def sign(self, auth: Any, token_endpoint: str) -> bytes:
|
||||
return private_key_jwt_sign(
|
||||
auth.client_secret,
|
||||
client_id=auth.client_id,
|
||||
token_endpoint=token_endpoint,
|
||||
claims=self.claims,
|
||||
header={"kid": auth.client_secret["kid"]},
|
||||
)
|
||||
|
||||
|
||||
class MSC3861DelegatedAuth(BaseAuth):
|
||||
AUTH_METHODS = {
|
||||
"client_secret_post": encode_client_secret_post,
|
||||
"client_secret_basic": encode_client_secret_basic,
|
||||
"client_secret_jwt": ClientSecretJWT(),
|
||||
"private_key_jwt": PrivateKeyJWTWithKid(),
|
||||
}
|
||||
|
||||
EXTERNAL_ID_PROVIDER = "oauth-delegated"
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
|
||||
self._config = hs.config.experimental.msc3861
|
||||
auth_method = MSC3861DelegatedAuth.AUTH_METHODS.get(
|
||||
self._config.client_auth_method.value, None
|
||||
)
|
||||
# Those assertions are already checked when parsing the config
|
||||
assert self._config.enabled, "OAuth delegation is not enabled"
|
||||
assert self._config.issuer, "No issuer provided"
|
||||
assert self._config.client_id, "No client_id provided"
|
||||
assert auth_method is not None, "Invalid client_auth_method provided"
|
||||
|
||||
self._http_client = hs.get_proxied_http_client()
|
||||
self._hostname = hs.hostname
|
||||
self._admin_token = self._config.admin_token
|
||||
|
||||
self._issuer_metadata = RetryOnExceptionCachedCall(self._load_metadata)
|
||||
|
||||
if isinstance(auth_method, PrivateKeyJWTWithKid):
|
||||
# Use the JWK as the client secret when using the private_key_jwt method
|
||||
assert self._config.jwk, "No JWK provided"
|
||||
self._client_auth = ClientAuth(
|
||||
self._config.client_id, self._config.jwk, auth_method
|
||||
)
|
||||
else:
|
||||
# Else use the client secret
|
||||
assert self._config.client_secret, "No client_secret provided"
|
||||
self._client_auth = ClientAuth(
|
||||
self._config.client_id, self._config.client_secret, auth_method
|
||||
)
|
||||
|
||||
async def _load_metadata(self) -> OpenIDProviderMetadata:
|
||||
if self._config.issuer_metadata is not None:
|
||||
return OpenIDProviderMetadata(**self._config.issuer_metadata)
|
||||
url = get_well_known_url(self._config.issuer, external=True)
|
||||
response = await self._http_client.get_json(url)
|
||||
metadata = OpenIDProviderMetadata(**response)
|
||||
# metadata.validate_introspection_endpoint()
|
||||
return metadata
|
||||
|
||||
async def _introspect_token(self, token: str) -> IntrospectionToken:
|
||||
"""
|
||||
Send a token to the introspection endpoint and returns the introspection response
|
||||
|
||||
Parameters:
|
||||
token: The token to introspect
|
||||
|
||||
Raises:
|
||||
HttpResponseException: If the introspection endpoint returns a non-2xx response
|
||||
ValueError: If the introspection endpoint returns an invalid JSON response
|
||||
JSONDecodeError: If the introspection endpoint returns a non-JSON response
|
||||
Exception: If the HTTP request fails
|
||||
|
||||
Returns:
|
||||
The introspection response
|
||||
"""
|
||||
metadata = await self._issuer_metadata.get()
|
||||
introspection_endpoint = metadata.get("introspection_endpoint")
|
||||
raw_headers: Dict[str, str] = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"User-Agent": str(self._http_client.user_agent, "utf-8"),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
args = {"token": token, "token_type_hint": "access_token"}
|
||||
body = urlencode(args, True)
|
||||
|
||||
# Fill the body/headers with credentials
|
||||
uri, raw_headers, body = self._client_auth.prepare(
|
||||
method="POST", uri=introspection_endpoint, headers=raw_headers, body=body
|
||||
)
|
||||
headers = Headers({k: [v] for (k, v) in raw_headers.items()})
|
||||
|
||||
# Do the actual request
|
||||
# We're not using the SimpleHttpClient util methods as we don't want to
|
||||
# check the HTTP status code, and we do the body encoding ourselves.
|
||||
response = await self._http_client.request(
|
||||
method="POST",
|
||||
uri=uri,
|
||||
data=body.encode("utf-8"),
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
resp_body = await make_deferred_yieldable(readBody(response))
|
||||
|
||||
if response.code < 200 or response.code >= 300:
|
||||
raise HttpResponseException(
|
||||
response.code,
|
||||
response.phrase.decode("ascii", errors="replace"),
|
||||
resp_body,
|
||||
)
|
||||
|
||||
resp = json_decoder.decode(resp_body.decode("utf-8"))
|
||||
|
||||
if not isinstance(resp, dict):
|
||||
raise ValueError(
|
||||
"The introspection endpoint returned an invalid JSON response."
|
||||
)
|
||||
|
||||
return IntrospectionToken(**resp)
|
||||
|
||||
async def is_server_admin(self, requester: Requester) -> bool:
|
||||
return "urn:synapse:admin:*" in requester.scope
|
||||
|
||||
async def get_user_by_req(
|
||||
self,
|
||||
request: SynapseRequest,
|
||||
allow_guest: bool = False,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
access_token = self.get_access_token_from_request(request)
|
||||
|
||||
requester = await self.get_appservice_user(request, access_token)
|
||||
if not requester:
|
||||
# TODO: we probably want to assert the allow_guest inside this call
|
||||
# so that we don't provision the user if they don't have enough permission:
|
||||
requester = await self.get_user_by_access_token(access_token, allow_expired)
|
||||
|
||||
if not allow_guest and requester.is_guest:
|
||||
raise OAuthInsufficientScopeError([SCOPE_MATRIX_API])
|
||||
|
||||
request.requester = requester
|
||||
|
||||
return requester
|
||||
|
||||
async def get_user_by_access_token(
|
||||
self,
|
||||
token: str,
|
||||
allow_expired: bool = False,
|
||||
) -> Requester:
|
||||
if self._admin_token is not None and token == self._admin_token:
|
||||
# XXX: This is a temporary solution so that the admin API can be called by
|
||||
# the OIDC provider. This will be removed once we have OIDC client
|
||||
# credentials grant support in matrix-authentication-service.
|
||||
logging.info("Admin toked used")
|
||||
# XXX: that user doesn't exist and won't be provisioned.
|
||||
# This is mostly fine for admin calls, but we should also think about doing
|
||||
# requesters without a user_id.
|
||||
admin_user = UserID("__oidc_admin", self._hostname)
|
||||
return create_requester(
|
||||
user_id=admin_user,
|
||||
scope=["urn:synapse:admin:*"],
|
||||
)
|
||||
|
||||
try:
|
||||
introspection_result = await self._introspect_token(token)
|
||||
except Exception:
|
||||
logger.exception("Failed to introspect token")
|
||||
raise SynapseError(503, "Unable to introspect the access token")
|
||||
|
||||
logger.info(f"Introspection result: {introspection_result!r}")
|
||||
|
||||
# TODO: introspection verification should be more extensive, especially:
|
||||
# - verify the audience
|
||||
if not introspection_result.get("active"):
|
||||
raise InvalidClientTokenError("Token is not active")
|
||||
|
||||
# Let's look at the scope
|
||||
scope: List[str] = scope_to_list(introspection_result.get("scope", ""))
|
||||
|
||||
# Determine type of user based on presence of particular scopes
|
||||
has_user_scope = SCOPE_MATRIX_API in scope
|
||||
has_guest_scope = SCOPE_MATRIX_GUEST in scope
|
||||
|
||||
if not has_user_scope and not has_guest_scope:
|
||||
raise InvalidClientTokenError("No scope in token granting user rights")
|
||||
|
||||
# Match via the sub claim
|
||||
sub: Optional[str] = introspection_result.get("sub")
|
||||
if sub is None:
|
||||
raise InvalidClientTokenError(
|
||||
"Invalid sub claim in the introspection result"
|
||||
)
|
||||
|
||||
user_id_str = await self.store.get_user_by_external_id(
|
||||
MSC3861DelegatedAuth.EXTERNAL_ID_PROVIDER, sub
|
||||
)
|
||||
if user_id_str is None:
|
||||
# If we could not find a user via the external_id, it either does not exist,
|
||||
# or the external_id was never recorded
|
||||
|
||||
# TODO: claim mapping should be configurable
|
||||
username: Optional[str] = introspection_result.get("username")
|
||||
if username is None or not isinstance(username, str):
|
||||
raise AuthError(
|
||||
500,
|
||||
"Invalid username claim in the introspection result",
|
||||
)
|
||||
user_id = UserID(username, self._hostname)
|
||||
|
||||
# First try to find a user from the username claim
|
||||
user_info = await self.store.get_userinfo_by_id(user_id=user_id.to_string())
|
||||
if user_info is None:
|
||||
# If the user does not exist, we should create it on the fly
|
||||
# TODO: we could use SCIM to provision users ahead of time and listen
|
||||
# for SCIM SET events if those ever become standard:
|
||||
# https://datatracker.ietf.org/doc/html/draft-hunt-scim-notify-00
|
||||
|
||||
# TODO: claim mapping should be configurable
|
||||
# If present, use the name claim as the displayname
|
||||
name: Optional[str] = introspection_result.get("name")
|
||||
|
||||
await self.store.register_user(
|
||||
user_id=user_id.to_string(), create_profile_with_displayname=name
|
||||
)
|
||||
|
||||
# And record the sub as external_id
|
||||
await self.store.record_user_external_id(
|
||||
MSC3861DelegatedAuth.EXTERNAL_ID_PROVIDER, sub, user_id.to_string()
|
||||
)
|
||||
else:
|
||||
user_id = UserID.from_string(user_id_str)
|
||||
|
||||
# Find device_ids in scope
|
||||
# We only allow a single device_id in the scope, so we find them all in the
|
||||
# scope list, and raise if there are more than one. The OIDC server should be
|
||||
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
|
||||
device_ids = [
|
||||
tok[len(SCOPE_MATRIX_DEVICE_PREFIX) :]
|
||||
for tok in scope
|
||||
if tok.startswith(SCOPE_MATRIX_DEVICE_PREFIX)
|
||||
]
|
||||
|
||||
if len(device_ids) > 1:
|
||||
raise AuthError(
|
||||
500,
|
||||
"Multiple device IDs in scope",
|
||||
)
|
||||
|
||||
device_id = device_ids[0] if device_ids else None
|
||||
if device_id is not None:
|
||||
# Sanity check the device_id
|
||||
if len(device_id) > 255 or len(device_id) < 1:
|
||||
raise AuthError(
|
||||
500,
|
||||
"Invalid device ID in scope",
|
||||
)
|
||||
|
||||
# Create the device on the fly if it does not exist
|
||||
try:
|
||||
await self.store.get_device(
|
||||
user_id=user_id.to_string(), device_id=device_id
|
||||
)
|
||||
except StoreError:
|
||||
await self.store.store_device(
|
||||
user_id=user_id.to_string(),
|
||||
device_id=device_id,
|
||||
initial_device_display_name="OIDC-native client",
|
||||
)
|
||||
|
||||
# TODO: there is a few things missing in the requester here, which still need
|
||||
# to be figured out, like:
|
||||
# - impersonation, with the `authenticated_entity`, which is used for
|
||||
# rate-limiting, MAU limits, etc.
|
||||
# - shadow-banning, with the `shadow_banned` flag
|
||||
# - a proper solution for appservices, which still needs to be figured out in
|
||||
# the context of MSC3861
|
||||
return create_requester(
|
||||
user_id=user_id,
|
||||
device_id=device_id,
|
||||
scope=scope,
|
||||
is_guest=(has_guest_scope and not has_user_scope),
|
||||
)
|
|
@ -236,7 +236,7 @@ class EventContentFields:
|
|||
AUTHORISING_USER: Final = "join_authorised_via_users_server"
|
||||
|
||||
# Use for mentioning users.
|
||||
MSC3952_MENTIONS: Final = "org.matrix.msc3952.mentions"
|
||||
MENTIONS: Final = "m.mentions"
|
||||
|
||||
# an unspecced field added to to-device messages to identify them uniquely-ish
|
||||
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
||||
|
|
|
@ -119,14 +119,20 @@ class Codes(str, Enum):
|
|||
|
||||
|
||||
class CodeMessageException(RuntimeError):
|
||||
"""An exception with integer code and message string attributes.
|
||||
"""An exception with integer code, a message string attributes and optional headers.
|
||||
|
||||
Attributes:
|
||||
code: HTTP error code
|
||||
msg: string describing the error
|
||||
headers: optional response headers to send
|
||||
"""
|
||||
|
||||
def __init__(self, code: Union[int, HTTPStatus], msg: str):
|
||||
def __init__(
|
||||
self,
|
||||
code: Union[int, HTTPStatus],
|
||||
msg: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
super().__init__("%d: %s" % (code, msg))
|
||||
|
||||
# Some calls to this method pass instances of http.HTTPStatus for `code`.
|
||||
|
@ -137,6 +143,7 @@ class CodeMessageException(RuntimeError):
|
|||
# To eliminate this behaviour, we convert them to their integer equivalents here.
|
||||
self.code = int(code)
|
||||
self.msg = msg
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class RedirectException(CodeMessageException):
|
||||
|
@ -182,6 +189,7 @@ class SynapseError(CodeMessageException):
|
|||
msg: str,
|
||||
errcode: str = Codes.UNKNOWN,
|
||||
additional_fields: Optional[Dict] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
"""Constructs a synapse error.
|
||||
|
||||
|
@ -190,7 +198,7 @@ class SynapseError(CodeMessageException):
|
|||
msg: The human-readable error message.
|
||||
errcode: The matrix error code e.g 'M_FORBIDDEN'
|
||||
"""
|
||||
super().__init__(code, msg)
|
||||
super().__init__(code, msg, headers)
|
||||
self.errcode = errcode
|
||||
if additional_fields is None:
|
||||
self._additional_fields: Dict = {}
|
||||
|
@ -335,6 +343,20 @@ class AuthError(SynapseError):
|
|||
super().__init__(code, msg, errcode, additional_fields)
|
||||
|
||||
|
||||
class OAuthInsufficientScopeError(SynapseError):
|
||||
"""An error raised when the caller does not have sufficient scope to perform the requested action"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
required_scopes: List[str],
|
||||
):
|
||||
headers = {
|
||||
"WWW-Authenticate": 'Bearer error="insufficient_scope", scope="%s"'
|
||||
% (" ".join(required_scopes))
|
||||
}
|
||||
super().__init__(401, "Insufficient scope", Codes.FORBIDDEN, None, headers)
|
||||
|
||||
|
||||
class UnstableSpecAuthError(AuthError):
|
||||
"""An error raised when a new error code is being proposed to replace a previous one.
|
||||
This error will return a "org.matrix.unstable.errcode" property with the new error code,
|
||||
|
|
|
@ -152,9 +152,9 @@ class Filtering:
|
|||
self.DEFAULT_FILTER_COLLECTION = FilterCollection(hs, {})
|
||||
|
||||
async def get_user_filter(
|
||||
self, user_localpart: str, filter_id: Union[int, str]
|
||||
self, user_id: UserID, filter_id: Union[int, str]
|
||||
) -> "FilterCollection":
|
||||
result = await self.store.get_user_filter(user_localpart, filter_id)
|
||||
result = await self.store.get_user_filter(user_id, filter_id)
|
||||
return FilterCollection(self._hs, result)
|
||||
|
||||
def add_user_filter(self, user_id: UserID, user_filter: JsonDict) -> Awaitable[int]:
|
||||
|
|
|
@ -29,7 +29,14 @@ class AuthConfig(Config):
|
|||
if password_config is None:
|
||||
password_config = {}
|
||||
|
||||
passwords_enabled = password_config.get("enabled", True)
|
||||
# The default value of password_config.enabled is True, unless msc3861 is enabled.
|
||||
msc3861_enabled = (
|
||||
config.get("experimental_features", {})
|
||||
.get("msc3861", {})
|
||||
.get("enabled", False)
|
||||
)
|
||||
passwords_enabled = password_config.get("enabled", not msc3861_enabled)
|
||||
|
||||
# 'only_for_reauth' allows users who have previously set a password to use it,
|
||||
# even though passwords would otherwise be disabled.
|
||||
passwords_for_reauth_only = passwords_enabled == "only_for_reauth"
|
||||
|
@ -53,3 +60,13 @@ class AuthConfig(Config):
|
|||
self.ui_auth_session_timeout = self.parse_duration(
|
||||
ui_auth.get("session_timeout", 0)
|
||||
)
|
||||
|
||||
# Logging in with an existing session.
|
||||
login_via_existing = config.get("login_via_existing_session", {})
|
||||
self.login_via_existing_enabled = login_via_existing.get("enabled", False)
|
||||
self.login_via_existing_require_ui_auth = login_via_existing.get(
|
||||
"require_ui_auth", True
|
||||
)
|
||||
self.login_via_existing_token_timeout = self.parse_duration(
|
||||
login_via_existing.get("token_timeout", "5m")
|
||||
)
|
||||
|
|
|
@ -12,15 +12,216 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Any, Optional
|
||||
import enum
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import attr
|
||||
import attr.validators
|
||||
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
||||
from synapse.config import ConfigError
|
||||
from synapse.config._base import Config
|
||||
from synapse.config._base import Config, RootConfig
|
||||
from synapse.types import JsonDict
|
||||
|
||||
# Determine whether authlib is installed.
|
||||
try:
|
||||
import authlib # noqa: F401
|
||||
|
||||
HAS_AUTHLIB = True
|
||||
except ImportError:
|
||||
HAS_AUTHLIB = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Only import this if we're type checking, as it might not be installed at runtime.
|
||||
from authlib.jose.rfc7517 import JsonWebKey
|
||||
|
||||
|
||||
class ClientAuthMethod(enum.Enum):
|
||||
"""List of supported client auth methods."""
|
||||
|
||||
CLIENT_SECRET_POST = "client_secret_post"
|
||||
CLIENT_SECRET_BASIC = "client_secret_basic"
|
||||
CLIENT_SECRET_JWT = "client_secret_jwt"
|
||||
PRIVATE_KEY_JWT = "private_key_jwt"
|
||||
|
||||
|
||||
def _parse_jwks(jwks: Optional[JsonDict]) -> Optional["JsonWebKey"]:
|
||||
"""A helper function to parse a JWK dict into a JsonWebKey."""
|
||||
|
||||
if jwks is None:
|
||||
return None
|
||||
|
||||
from authlib.jose.rfc7517 import JsonWebKey
|
||||
|
||||
return JsonWebKey.import_key(jwks)
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class MSC3861:
|
||||
"""Configuration for MSC3861: Matrix architecture change to delegate authentication via OIDC"""
|
||||
|
||||
enabled: bool = attr.ib(default=False, validator=attr.validators.instance_of(bool))
|
||||
"""Whether to enable MSC3861 auth delegation."""
|
||||
|
||||
@enabled.validator
|
||||
def _check_enabled(self, attribute: attr.Attribute, value: bool) -> None:
|
||||
# Only allow enabling MSC3861 if authlib is installed
|
||||
if value and not HAS_AUTHLIB:
|
||||
raise ConfigError(
|
||||
"MSC3861 is enabled but authlib is not installed. "
|
||||
"Please install authlib to use MSC3861.",
|
||||
("experimental", "msc3861", "enabled"),
|
||||
)
|
||||
|
||||
issuer: str = attr.ib(default="", validator=attr.validators.instance_of(str))
|
||||
"""The URL of the OIDC Provider."""
|
||||
|
||||
issuer_metadata: Optional[JsonDict] = attr.ib(default=None)
|
||||
"""The issuer metadata to use, otherwise discovered from /.well-known/openid-configuration as per MSC2965."""
|
||||
|
||||
client_id: str = attr.ib(
|
||||
default="",
|
||||
validator=attr.validators.instance_of(str),
|
||||
)
|
||||
"""The client ID to use when calling the introspection endpoint."""
|
||||
|
||||
client_auth_method: ClientAuthMethod = attr.ib(
|
||||
default=ClientAuthMethod.CLIENT_SECRET_POST, converter=ClientAuthMethod
|
||||
)
|
||||
"""The auth method used when calling the introspection endpoint."""
|
||||
|
||||
client_secret: Optional[str] = attr.ib(
|
||||
default=None,
|
||||
validator=attr.validators.optional(attr.validators.instance_of(str)),
|
||||
)
|
||||
"""
|
||||
The client secret to use when calling the introspection endpoint,
|
||||
when using any of the client_secret_* client auth methods.
|
||||
"""
|
||||
|
||||
jwk: Optional["JsonWebKey"] = attr.ib(default=None, converter=_parse_jwks)
|
||||
"""
|
||||
The JWKS to use when calling the introspection endpoint,
|
||||
when using the private_key_jwt client auth method.
|
||||
"""
|
||||
|
||||
@client_auth_method.validator
|
||||
def _check_client_auth_method(
|
||||
self, attribute: attr.Attribute, value: ClientAuthMethod
|
||||
) -> None:
|
||||
# Check that the right client credentials are provided for the client auth method.
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if value == ClientAuthMethod.PRIVATE_KEY_JWT and self.jwk is None:
|
||||
raise ConfigError(
|
||||
"A JWKS must be provided when using the private_key_jwt client auth method",
|
||||
("experimental", "msc3861", "client_auth_method"),
|
||||
)
|
||||
|
||||
if (
|
||||
value
|
||||
in (
|
||||
ClientAuthMethod.CLIENT_SECRET_POST,
|
||||
ClientAuthMethod.CLIENT_SECRET_BASIC,
|
||||
ClientAuthMethod.CLIENT_SECRET_JWT,
|
||||
)
|
||||
and self.client_secret is None
|
||||
):
|
||||
raise ConfigError(
|
||||
f"A client secret must be provided when using the {value} client auth method",
|
||||
("experimental", "msc3861", "client_auth_method"),
|
||||
)
|
||||
|
||||
account_management_url: Optional[str] = attr.ib(
|
||||
default=None,
|
||||
validator=attr.validators.optional(attr.validators.instance_of(str)),
|
||||
)
|
||||
"""The URL of the My Account page on the OIDC Provider as per MSC2965."""
|
||||
|
||||
admin_token: Optional[str] = attr.ib(
|
||||
default=None,
|
||||
validator=attr.validators.optional(attr.validators.instance_of(str)),
|
||||
)
|
||||
"""
|
||||
A token that should be considered as an admin token.
|
||||
This is used by the OIDC provider, to make admin calls to Synapse.
|
||||
"""
|
||||
|
||||
def check_config_conflicts(self, root: RootConfig) -> None:
|
||||
"""Checks for any configuration conflicts with other parts of Synapse.
|
||||
|
||||
Raises:
|
||||
ConfigError: If there are any configuration conflicts.
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if (
|
||||
root.auth.password_enabled_for_reauth
|
||||
or root.auth.password_enabled_for_login
|
||||
):
|
||||
raise ConfigError(
|
||||
"Password auth cannot be enabled when OAuth delegation is enabled",
|
||||
("password_config", "enabled"),
|
||||
)
|
||||
|
||||
if root.registration.enable_registration:
|
||||
raise ConfigError(
|
||||
"Registration cannot be enabled when OAuth delegation is enabled",
|
||||
("enable_registration",),
|
||||
)
|
||||
|
||||
if (
|
||||
root.oidc.oidc_enabled
|
||||
or root.saml2.saml2_enabled
|
||||
or root.cas.cas_enabled
|
||||
or root.jwt.jwt_enabled
|
||||
):
|
||||
raise ConfigError("SSO cannot be enabled when OAuth delegation is enabled")
|
||||
|
||||
if bool(root.authproviders.password_providers):
|
||||
raise ConfigError(
|
||||
"Password auth providers cannot be enabled when OAuth delegation is enabled"
|
||||
)
|
||||
|
||||
if root.captcha.enable_registration_captcha:
|
||||
raise ConfigError(
|
||||
"CAPTCHA cannot be enabled when OAuth delegation is enabled",
|
||||
("captcha", "enable_registration_captcha"),
|
||||
)
|
||||
|
||||
if root.auth.login_via_existing_enabled:
|
||||
raise ConfigError(
|
||||
"Login via existing session cannot be enabled when OAuth delegation is enabled",
|
||||
("login_via_existing_session", "enabled"),
|
||||
)
|
||||
|
||||
if root.registration.refresh_token_lifetime:
|
||||
raise ConfigError(
|
||||
"refresh_token_lifetime cannot be set when OAuth delegation is enabled",
|
||||
("refresh_token_lifetime",),
|
||||
)
|
||||
|
||||
if root.registration.nonrefreshable_access_token_lifetime:
|
||||
raise ConfigError(
|
||||
"nonrefreshable_access_token_lifetime cannot be set when OAuth delegation is enabled",
|
||||
("nonrefreshable_access_token_lifetime",),
|
||||
)
|
||||
|
||||
if root.registration.session_lifetime:
|
||||
raise ConfigError(
|
||||
"session_lifetime cannot be set when OAuth delegation is enabled",
|
||||
("session_lifetime",),
|
||||
)
|
||||
|
||||
if not root.experimental.msc3970_enabled:
|
||||
raise ConfigError(
|
||||
"experimental_features.msc3970_enabled must be 'true' when OAuth delegation is enabled",
|
||||
("experimental_features", "msc3970_enabled"),
|
||||
)
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
||||
class MSC3866Config:
|
||||
|
@ -118,13 +319,6 @@ class ExperimentalConfig(Config):
|
|||
# MSC3881: Remotely toggle push notifications for another client
|
||||
self.msc3881_enabled: bool = experimental.get("msc3881_enabled", False)
|
||||
|
||||
# MSC3882: Allow an existing session to sign in a new session
|
||||
self.msc3882_enabled: bool = experimental.get("msc3882_enabled", False)
|
||||
self.msc3882_ui_auth: bool = experimental.get("msc3882_ui_auth", True)
|
||||
self.msc3882_token_timeout = self.parse_duration(
|
||||
experimental.get("msc3882_token_timeout", "5m")
|
||||
)
|
||||
|
||||
# MSC3874: Filtering /messages with rel_types / not_rel_types.
|
||||
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)
|
||||
|
||||
|
@ -164,11 +358,6 @@ class ExperimentalConfig(Config):
|
|||
# MSC3391: Removing account data.
|
||||
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
||||
|
||||
# MSC3952: Intentional mentions, this depends on MSC3966.
|
||||
self.msc3952_intentional_mentions = experimental.get(
|
||||
"msc3952_intentional_mentions", False
|
||||
)
|
||||
|
||||
# MSC3959: Do not generate notifications for edits.
|
||||
self.msc3958_supress_edit_notifs = experimental.get(
|
||||
"msc3958_supress_edit_notifs", False
|
||||
|
@ -182,8 +371,19 @@ class ExperimentalConfig(Config):
|
|||
"msc3981_recurse_relations", False
|
||||
)
|
||||
|
||||
# MSC3861: Matrix architecture change to delegate authentication via OIDC
|
||||
try:
|
||||
self.msc3861 = MSC3861(**experimental.get("msc3861", {}))
|
||||
except ValueError as exc:
|
||||
raise ConfigError(
|
||||
"Invalid MSC3861 configuration", ("experimental", "msc3861")
|
||||
) from exc
|
||||
|
||||
# MSC3970: Scope transaction IDs to devices
|
||||
self.msc3970_enabled = experimental.get("msc3970_enabled", False)
|
||||
self.msc3970_enabled = experimental.get("msc3970_enabled", self.msc3861.enabled)
|
||||
|
||||
# Check that none of the other config options conflict with MSC3861 when enabled
|
||||
self.msc3861.check_config_conflicts(self.root)
|
||||
|
||||
# MSC4009: E.164 Matrix IDs
|
||||
self.msc4009_e164_mxids = experimental.get("msc4009_e164_mxids", False)
|
||||
|
|
|
@ -22,6 +22,8 @@ class FederationConfig(Config):
|
|||
section = "federation"
|
||||
|
||||
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
||||
federation_config = config.setdefault("federation", {})
|
||||
|
||||
# FIXME: federation_domain_whitelist needs sytests
|
||||
self.federation_domain_whitelist: Optional[dict] = None
|
||||
federation_domain_whitelist = config.get("federation_domain_whitelist", None)
|
||||
|
@ -49,5 +51,13 @@ class FederationConfig(Config):
|
|||
"allow_device_name_lookup_over_federation", False
|
||||
)
|
||||
|
||||
# Allow for the configuration of timeout, max request retries
|
||||
# and min/max retry delays in the matrix federation client.
|
||||
self.client_timeout = federation_config.get("client_timeout", 60)
|
||||
self.max_long_retry_delay = federation_config.get("max_long_retry_delay", 60)
|
||||
self.max_short_retry_delay = federation_config.get("max_short_retry_delay", 2)
|
||||
self.max_long_retries = federation_config.get("max_long_retries", 10)
|
||||
self.max_short_retries = federation_config.get("max_short_retries", 3)
|
||||
|
||||
|
||||
_METRICS_FOR_DOMAINS_SCHEMA = {"type": "array", "items": {"type": "string"}}
|
||||
|
|
|
@ -134,13 +134,8 @@ class EventValidator:
|
|||
)
|
||||
|
||||
# If the event contains a mentions key, validate it.
|
||||
if (
|
||||
EventContentFields.MSC3952_MENTIONS in event.content
|
||||
and config.experimental.msc3952_intentional_mentions
|
||||
):
|
||||
validate_json_object(
|
||||
event.content[EventContentFields.MSC3952_MENTIONS], Mentions
|
||||
)
|
||||
if EventContentFields.MENTIONS in event.content:
|
||||
validate_json_object(event.content[EventContentFields.MENTIONS], Mentions)
|
||||
|
||||
def _validate_retention(self, event: EventBase) -> None:
|
||||
"""Checks that an event that defines the retention policy for a room respects the
|
||||
|
|
|
@ -515,7 +515,7 @@ class FederationServer(FederationBase):
|
|||
logger.error(
|
||||
"Failed to handle PDU %s",
|
||||
event_id,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
return {"error": str(e)}
|
||||
|
||||
|
@ -944,7 +944,7 @@ class FederationServer(FederationBase):
|
|||
if not self._is_mine_server_name(authorising_server):
|
||||
raise SynapseError(
|
||||
400,
|
||||
f"Cannot authorise request from resident server: {authorising_server}",
|
||||
f"Cannot authorise membership event for {authorising_server}. We can only authorise requests from our own homeserver",
|
||||
)
|
||||
|
||||
event.signatures.update(
|
||||
|
@ -1247,7 +1247,7 @@ class FederationServer(FederationBase):
|
|||
logger.error(
|
||||
"Failed to handle PDU %s",
|
||||
event.event_id,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
|
||||
received_ts = await self.store.remove_received_event_from_staging(
|
||||
|
@ -1291,9 +1291,6 @@ class FederationServer(FederationBase):
|
|||
return
|
||||
lock = new_lock
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "<ReplicationLayer(%s)>" % self.server_name
|
||||
|
||||
async def exchange_third_party_invite(
|
||||
self, sender_user_id: str, target_user_id: str, room_id: str, signed: Dict
|
||||
) -> None:
|
||||
|
|
|
@ -164,7 +164,7 @@ class AccountValidityHandler:
|
|||
|
||||
try:
|
||||
user_display_name = await self.store.get_profile_displayname(
|
||||
UserID.from_string(user_id).localpart
|
||||
UserID.from_string(user_id)
|
||||
)
|
||||
if user_display_name is None:
|
||||
user_display_name = user_id
|
||||
|
|
|
@ -89,7 +89,7 @@ class AdminHandler:
|
|||
}
|
||||
|
||||
# Add additional user metadata
|
||||
profile = await self._store.get_profileinfo(user.localpart)
|
||||
profile = await self._store.get_profileinfo(user)
|
||||
threepids = await self._store.user_get_threepids(user.to_string())
|
||||
external_ids = [
|
||||
({"auth_provider": auth_provider, "external_id": external_id})
|
||||
|
|
|
@ -274,6 +274,8 @@ class AuthHandler:
|
|||
# response.
|
||||
self._extra_attributes: Dict[str, SsoLoginExtraAttributes] = {}
|
||||
|
||||
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||
|
||||
async def validate_user_via_ui_auth(
|
||||
self,
|
||||
requester: Requester,
|
||||
|
@ -322,8 +324,12 @@ class AuthHandler:
|
|||
|
||||
LimitExceededError if the ratelimiter's failed request count for this
|
||||
user is too high to proceed
|
||||
|
||||
"""
|
||||
if self.msc3861_oauth_delegation_enabled:
|
||||
raise SynapseError(
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR, "UIA shouldn't be used with MSC3861"
|
||||
)
|
||||
|
||||
if not requester.access_token_id:
|
||||
raise ValueError("Cannot validate a user without an access token")
|
||||
if can_skip_ui_auth and self._ui_auth_session_timeout:
|
||||
|
@ -1753,7 +1759,7 @@ class AuthHandler:
|
|||
return
|
||||
|
||||
user_profile_data = await self.store.get_profileinfo(
|
||||
UserID.from_string(registered_user_id).localpart
|
||||
UserID.from_string(registered_user_id)
|
||||
)
|
||||
|
||||
# Store any extra attributes which will be passed in the login response.
|
||||
|
|
|
@ -297,5 +297,5 @@ class DeactivateAccountHandler:
|
|||
# Add the user to the directory, if necessary. Note that
|
||||
# this must be done after the user is re-activated, because
|
||||
# deactivated users are excluded from the user directory.
|
||||
profile = await self.store.get_profileinfo(user.localpart)
|
||||
profile = await self.store.get_profileinfo(user)
|
||||
await self.user_directory_handler.handle_local_profile_change(user_id, profile)
|
||||
|
|
|
@ -200,6 +200,7 @@ class FederationHandler:
|
|||
)
|
||||
|
||||
@trace
|
||||
@tag_args
|
||||
async def maybe_backfill(
|
||||
self, room_id: str, current_depth: int, limit: int
|
||||
) -> bool:
|
||||
|
@ -214,6 +215,9 @@ class FederationHandler:
|
|||
limit: The number of events that the pagination request will
|
||||
return. This is used as part of the heuristic to decide if we
|
||||
should back paginate.
|
||||
|
||||
Returns:
|
||||
True if we actually tried to backfill something, otherwise False.
|
||||
"""
|
||||
# Starting the processing time here so we can include the room backfill
|
||||
# linearizer lock queue in the timing
|
||||
|
@ -227,6 +231,8 @@ class FederationHandler:
|
|||
processing_start_time=processing_start_time,
|
||||
)
|
||||
|
||||
@trace
|
||||
@tag_args
|
||||
async def _maybe_backfill_inner(
|
||||
self,
|
||||
room_id: str,
|
||||
|
@ -247,6 +253,9 @@ class FederationHandler:
|
|||
limit: The max number of events to request from the remote federated server.
|
||||
processing_start_time: The time when `maybe_backfill` started processing.
|
||||
Only used for timing. If `None`, no timing observation will be made.
|
||||
|
||||
Returns:
|
||||
True if we actually tried to backfill something, otherwise False.
|
||||
"""
|
||||
backwards_extremities = [
|
||||
_BackfillPoint(event_id, depth, _BackfillPointType.BACKWARDS_EXTREMITY)
|
||||
|
@ -302,15 +311,30 @@ class FederationHandler:
|
|||
len(sorted_backfill_points),
|
||||
sorted_backfill_points,
|
||||
)
|
||||
set_tag(
|
||||
SynapseTags.RESULT_PREFIX + "sorted_backfill_points",
|
||||
str(sorted_backfill_points),
|
||||
)
|
||||
set_tag(
|
||||
SynapseTags.RESULT_PREFIX + "sorted_backfill_points.length",
|
||||
str(len(sorted_backfill_points)),
|
||||
)
|
||||
|
||||
# If we have no backfill points lower than the `current_depth` then
|
||||
# either we can a) bail or b) still attempt to backfill. We opt to try
|
||||
# backfilling anyway just in case we do get relevant events.
|
||||
# If we have no backfill points lower than the `current_depth` then either we
|
||||
# can a) bail or b) still attempt to backfill. We opt to try backfilling anyway
|
||||
# just in case we do get relevant events. This is good for eventual consistency
|
||||
# sake but we don't need to block the client for something that is just as
|
||||
# likely not to return anything relevant so we backfill in the background. The
|
||||
# only way, this could return something relevant is if we discover a new branch
|
||||
# of history that extends all the way back to where we are currently paginating
|
||||
# and it's within the 100 events that are returned from `/backfill`.
|
||||
if not sorted_backfill_points and current_depth != MAX_DEPTH:
|
||||
logger.debug(
|
||||
"_maybe_backfill_inner: all backfill points are *after* current depth. Trying again with later backfill points."
|
||||
)
|
||||
return await self._maybe_backfill_inner(
|
||||
run_as_background_process(
|
||||
"_maybe_backfill_inner_anyway_with_max_depth",
|
||||
self._maybe_backfill_inner,
|
||||
room_id=room_id,
|
||||
# We use `MAX_DEPTH` so that we find all backfill points next
|
||||
# time (all events are below the `MAX_DEPTH`)
|
||||
|
@ -321,6 +345,9 @@ class FederationHandler:
|
|||
# overall otherwise the smaller one will throw off the results.
|
||||
processing_start_time=None,
|
||||
)
|
||||
# We return `False` because we're backfilling in the background and there is
|
||||
# no new events immediately for the caller to know about yet.
|
||||
return False
|
||||
|
||||
# Even after recursing with `MAX_DEPTH`, we didn't find any
|
||||
# backward extremities to backfill from.
|
||||
|
|
|
@ -1354,7 +1354,7 @@ class OidcProvider:
|
|||
finish_request(request)
|
||||
|
||||
|
||||
class LogoutToken(JWTClaims):
|
||||
class LogoutToken(JWTClaims): # type: ignore[misc]
|
||||
"""
|
||||
Holds and verify claims of a logout token, as per
|
||||
https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken
|
||||
|
|
|
@ -360,7 +360,7 @@ class PaginationHandler:
|
|||
except Exception:
|
||||
f = Failure()
|
||||
logger.error(
|
||||
"[purge] failed", exc_info=(f.type, f.value, f.getTracebackObject()) # type: ignore
|
||||
"[purge] failed", exc_info=(f.type, f.value, f.getTracebackObject())
|
||||
)
|
||||
self._purges_by_id[purge_id].status = PurgeStatus.STATUS_FAILED
|
||||
self._purges_by_id[purge_id].error = f.getErrorMessage()
|
||||
|
@ -689,7 +689,7 @@ class PaginationHandler:
|
|||
f = Failure()
|
||||
logger.error(
|
||||
"failed",
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
self._delete_by_id[delete_id].status = DeleteStatus.STATUS_FAILED
|
||||
self._delete_by_id[delete_id].error = f.getErrorMessage()
|
||||
|
|
|
@ -648,7 +648,6 @@ class PresenceHandler(BasePresenceHandler):
|
|||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.hs = hs
|
||||
self.server_name = hs.hostname
|
||||
self.wheel_timer: WheelTimer[str] = WheelTimer()
|
||||
self.notifier = hs.get_notifier()
|
||||
self._presence_enabled = hs.config.server.use_presence
|
||||
|
|
|
@ -67,7 +67,7 @@ class ProfileHandler:
|
|||
target_user = UserID.from_string(user_id)
|
||||
|
||||
if self.hs.is_mine(target_user):
|
||||
profileinfo = await self.store.get_profileinfo(target_user.localpart)
|
||||
profileinfo = await self.store.get_profileinfo(target_user)
|
||||
if profileinfo.display_name is None:
|
||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||
|
||||
|
@ -99,9 +99,7 @@ class ProfileHandler:
|
|||
async def get_displayname(self, target_user: UserID) -> Optional[str]:
|
||||
if self.hs.is_mine(target_user):
|
||||
try:
|
||||
displayname = await self.store.get_profile_displayname(
|
||||
target_user.localpart
|
||||
)
|
||||
displayname = await self.store.get_profile_displayname(target_user)
|
||||
except StoreError as e:
|
||||
if e.code == 404:
|
||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||
|
@ -147,7 +145,7 @@ class ProfileHandler:
|
|||
raise AuthError(400, "Cannot set another user's displayname")
|
||||
|
||||
if not by_admin and not self.hs.config.registration.enable_set_displayname:
|
||||
profile = await self.store.get_profileinfo(target_user.localpart)
|
||||
profile = await self.store.get_profileinfo(target_user)
|
||||
if profile.display_name:
|
||||
raise SynapseError(
|
||||
400,
|
||||
|
@ -180,7 +178,7 @@ class ProfileHandler:
|
|||
|
||||
await self.store.set_profile_displayname(target_user, displayname_to_set)
|
||||
|
||||
profile = await self.store.get_profileinfo(target_user.localpart)
|
||||
profile = await self.store.get_profileinfo(target_user)
|
||||
await self.user_directory_handler.handle_local_profile_change(
|
||||
target_user.to_string(), profile
|
||||
)
|
||||
|
@ -194,9 +192,7 @@ class ProfileHandler:
|
|||
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
|
||||
if self.hs.is_mine(target_user):
|
||||
try:
|
||||
avatar_url = await self.store.get_profile_avatar_url(
|
||||
target_user.localpart
|
||||
)
|
||||
avatar_url = await self.store.get_profile_avatar_url(target_user)
|
||||
except StoreError as e:
|
||||
if e.code == 404:
|
||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||
|
@ -241,7 +237,7 @@ class ProfileHandler:
|
|||
raise AuthError(400, "Cannot set another user's avatar_url")
|
||||
|
||||
if not by_admin and not self.hs.config.registration.enable_set_avatar_url:
|
||||
profile = await self.store.get_profileinfo(target_user.localpart)
|
||||
profile = await self.store.get_profileinfo(target_user)
|
||||
if profile.avatar_url:
|
||||
raise SynapseError(
|
||||
400, "Changing avatar is disabled on this server", Codes.FORBIDDEN
|
||||
|
@ -272,7 +268,7 @@ class ProfileHandler:
|
|||
|
||||
await self.store.set_profile_avatar_url(target_user, avatar_url_to_set)
|
||||
|
||||
profile = await self.store.get_profileinfo(target_user.localpart)
|
||||
profile = await self.store.get_profileinfo(target_user)
|
||||
await self.user_directory_handler.handle_local_profile_change(
|
||||
target_user.to_string(), profile
|
||||
)
|
||||
|
@ -369,14 +365,10 @@ class ProfileHandler:
|
|||
response = {}
|
||||
try:
|
||||
if just_field is None or just_field == "displayname":
|
||||
response["displayname"] = await self.store.get_profile_displayname(
|
||||
user.localpart
|
||||
)
|
||||
response["displayname"] = await self.store.get_profile_displayname(user)
|
||||
|
||||
if just_field is None or just_field == "avatar_url":
|
||||
response["avatar_url"] = await self.store.get_profile_avatar_url(
|
||||
user.localpart
|
||||
)
|
||||
response["avatar_url"] = await self.store.get_profile_avatar_url(user)
|
||||
except StoreError as e:
|
||||
if e.code == 404:
|
||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||
|
|
|
@ -27,7 +27,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class ReadMarkerHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.server_name = hs.config.server.server_name
|
||||
self.store = hs.get_datastores().main
|
||||
self.account_data_handler = hs.get_account_data_handler()
|
||||
self.read_marker_linearizer = Linearizer(name="read_marker")
|
||||
|
|
|
@ -315,7 +315,7 @@ class RegistrationHandler:
|
|||
approved=approved,
|
||||
)
|
||||
|
||||
profile = await self.store.get_profileinfo(localpart)
|
||||
profile = await self.store.get_profileinfo(user)
|
||||
await self.user_directory_handler.handle_local_profile_change(
|
||||
user_id, profile
|
||||
)
|
||||
|
|
|
@ -205,16 +205,22 @@ class RelationsHandler:
|
|||
event_id: The event IDs to look and redact relations of.
|
||||
initial_redaction_event: The redaction for the event referred to by
|
||||
event_id.
|
||||
relation_types: The types of relations to look for.
|
||||
relation_types: The types of relations to look for. If "*" is in the list,
|
||||
all related events will be redacted regardless of the type.
|
||||
|
||||
Raises:
|
||||
ShadowBanError if the requester is shadow-banned
|
||||
"""
|
||||
related_event_ids = (
|
||||
await self._main_store.get_all_relations_for_event_with_types(
|
||||
event_id, relation_types
|
||||
if "*" in relation_types:
|
||||
related_event_ids = await self._main_store.get_all_relations_for_event(
|
||||
event_id
|
||||
)
|
||||
else:
|
||||
related_event_ids = (
|
||||
await self._main_store.get_all_relations_for_event_with_types(
|
||||
event_id, relation_types
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for related_event_id in related_event_ids:
|
||||
try:
|
||||
|
|
|
@ -872,6 +872,8 @@ class RoomCreationHandler:
|
|||
visibility = config.get("visibility", "private")
|
||||
is_public = visibility == "public"
|
||||
|
||||
self._validate_room_config(config, visibility)
|
||||
|
||||
room_id = await self._generate_and_create_room_id(
|
||||
creator_id=user_id,
|
||||
is_public=is_public,
|
||||
|
@ -1111,20 +1113,7 @@ class RoomCreationHandler:
|
|||
|
||||
return new_event, new_unpersisted_context
|
||||
|
||||
visibility = room_config.get("visibility", "private")
|
||||
preset_config = room_config.get(
|
||||
"preset",
|
||||
RoomCreationPreset.PRIVATE_CHAT
|
||||
if visibility == "private"
|
||||
else RoomCreationPreset.PUBLIC_CHAT,
|
||||
)
|
||||
|
||||
try:
|
||||
config = self._presets_dict[preset_config]
|
||||
except KeyError:
|
||||
raise SynapseError(
|
||||
400, f"'{preset_config}' is not a valid preset", errcode=Codes.BAD_JSON
|
||||
)
|
||||
preset_config, config = self._room_preset_config(room_config)
|
||||
|
||||
# MSC2175 removes the creator field from the create event.
|
||||
if not room_version.msc2175_implicit_room_creator:
|
||||
|
@ -1306,6 +1295,65 @@ class RoomCreationHandler:
|
|||
assert last_event.internal_metadata.stream_ordering is not None
|
||||
return last_event.internal_metadata.stream_ordering, last_event.event_id, depth
|
||||
|
||||
def _validate_room_config(
|
||||
self,
|
||||
config: JsonDict,
|
||||
visibility: str,
|
||||
) -> None:
|
||||
"""Checks configuration parameters for a /createRoom request.
|
||||
|
||||
If validation detects invalid parameters an exception may be raised to
|
||||
cause room creation to be aborted and an error response to be returned
|
||||
to the client.
|
||||
|
||||
Args:
|
||||
config: A dict of configuration options. Originally from the body of
|
||||
the /createRoom request
|
||||
visibility: One of "public" or "private"
|
||||
"""
|
||||
|
||||
# Validate the requested preset, raise a 400 error if not valid
|
||||
preset_name, preset_config = self._room_preset_config(config)
|
||||
|
||||
# If the user is trying to create an encrypted room and this is forbidden
|
||||
# by the configured default_power_level_content_override, then reject the
|
||||
# request before the room is created.
|
||||
raw_initial_state = config.get("initial_state", [])
|
||||
room_encryption_event = any(
|
||||
s.get("type", "") == EventTypes.RoomEncryption for s in raw_initial_state
|
||||
)
|
||||
|
||||
if preset_config["encrypted"] or room_encryption_event:
|
||||
if self._default_power_level_content_override:
|
||||
override = self._default_power_level_content_override.get(preset_name)
|
||||
if override is not None:
|
||||
event_levels = override.get("events", {})
|
||||
room_admin_level = event_levels.get(EventTypes.PowerLevels, 100)
|
||||
encryption_level = event_levels.get(EventTypes.RoomEncryption, 100)
|
||||
if encryption_level > room_admin_level:
|
||||
raise SynapseError(
|
||||
403,
|
||||
f"You cannot create an encrypted room. user_level ({room_admin_level}) < send_level ({encryption_level})",
|
||||
)
|
||||
|
||||
def _room_preset_config(self, room_config: JsonDict) -> Tuple[str, dict]:
|
||||
# The spec says rooms should default to private visibility if
|
||||
# `visibility` is not specified.
|
||||
visibility = room_config.get("visibility", "private")
|
||||
preset_name = room_config.get(
|
||||
"preset",
|
||||
RoomCreationPreset.PRIVATE_CHAT
|
||||
if visibility == "private"
|
||||
else RoomCreationPreset.PUBLIC_CHAT,
|
||||
)
|
||||
try:
|
||||
preset_config = self._presets_dict[preset_name]
|
||||
except KeyError:
|
||||
raise SynapseError(
|
||||
400, f"'{preset_name}' is not a valid preset", errcode=Codes.BAD_JSON
|
||||
)
|
||||
return preset_name, preset_config
|
||||
|
||||
def _generate_room_id(self) -> str:
|
||||
"""Generates a random room ID.
|
||||
|
||||
|
@ -1490,7 +1538,6 @@ class RoomContextHandler:
|
|||
|
||||
class TimestampLookupHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.server_name = hs.hostname
|
||||
self.store = hs.get_datastores().main
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.federation_client = hs.get_federation_client()
|
||||
|
|
|
@ -42,7 +42,6 @@ class StatsHandler:
|
|||
self.store = hs.get_datastores().main
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self.state = hs.get_state_handler()
|
||||
self.server_name = hs.hostname
|
||||
self.clock = hs.get_clock()
|
||||
self.notifier = hs.get_notifier()
|
||||
self.is_mine_id = hs.is_mine_id
|
||||
|
|
|
@ -95,8 +95,6 @@ incoming_responses_counter = Counter(
|
|||
)
|
||||
|
||||
|
||||
MAX_LONG_RETRIES = 10
|
||||
MAX_SHORT_RETRIES = 3
|
||||
MAXINT = sys.maxsize
|
||||
|
||||
|
||||
|
@ -406,7 +404,12 @@ class MatrixFederationHttpClient:
|
|||
self.clock = hs.get_clock()
|
||||
self._store = hs.get_datastores().main
|
||||
self.version_string_bytes = hs.version_string.encode("ascii")
|
||||
self.default_timeout = 60
|
||||
self.default_timeout = hs.config.federation.client_timeout
|
||||
|
||||
self.max_long_retry_delay = hs.config.federation.max_long_retry_delay
|
||||
self.max_short_retry_delay = hs.config.federation.max_short_retry_delay
|
||||
self.max_long_retries = hs.config.federation.max_long_retries
|
||||
self.max_short_retries = hs.config.federation.max_short_retries
|
||||
|
||||
self._cooperator = Cooperator(scheduler=_make_scheduler(self.reactor))
|
||||
|
||||
|
@ -499,8 +502,15 @@ class MatrixFederationHttpClient:
|
|||
Note that the above intervals are *in addition* to the time spent
|
||||
waiting for the request to complete (up to `timeout` ms).
|
||||
|
||||
NB: the long retry algorithm takes over 20 minutes to complete, with
|
||||
a default timeout of 60s!
|
||||
NB: the long retry algorithm takes over 20 minutes to complete, with a
|
||||
default timeout of 60s! It's best not to use the `long_retries` option
|
||||
for something that is blocking a client so we don't make them wait for
|
||||
aaaaages, whereas some things like sending transactions (server to
|
||||
server) we can be a lot more lenient but its very fuzzy / hand-wavey.
|
||||
|
||||
In the future, we could be more intelligent about doing this sort of
|
||||
thing by looking at things with the bigger picture in mind,
|
||||
https://github.com/matrix-org/synapse/issues/8917
|
||||
|
||||
ignore_backoff: true to ignore the historical backoff data
|
||||
and try the request anyway.
|
||||
|
@ -576,9 +586,9 @@ class MatrixFederationHttpClient:
|
|||
# XXX: Would be much nicer to retry only at the transaction-layer
|
||||
# (once we have reliable transactions in place)
|
||||
if long_retries:
|
||||
retries_left = MAX_LONG_RETRIES
|
||||
retries_left = self.max_long_retries
|
||||
else:
|
||||
retries_left = MAX_SHORT_RETRIES
|
||||
retries_left = self.max_short_retries
|
||||
|
||||
url_bytes = request.uri
|
||||
url_str = url_bytes.decode("ascii")
|
||||
|
@ -723,12 +733,12 @@ class MatrixFederationHttpClient:
|
|||
|
||||
if retries_left and not timeout:
|
||||
if long_retries:
|
||||
delay = 4 ** (MAX_LONG_RETRIES + 1 - retries_left)
|
||||
delay = min(delay, 60)
|
||||
delay = 4 ** (self.max_long_retries + 1 - retries_left)
|
||||
delay = min(delay, self.max_long_retry_delay)
|
||||
delay *= random.uniform(0.8, 1.4)
|
||||
else:
|
||||
delay = 0.5 * 2 ** (MAX_SHORT_RETRIES - retries_left)
|
||||
delay = min(delay, 2)
|
||||
delay = 0.5 * 2 ** (self.max_short_retries - retries_left)
|
||||
delay = min(delay, self.max_short_retry_delay)
|
||||
delay *= random.uniform(0.8, 1.4)
|
||||
|
||||
logger.debug(
|
||||
|
|
|
@ -76,7 +76,7 @@ class ReplicationEndpointFactory:
|
|||
endpoint = wrapClientTLS(
|
||||
# The 'port' argument below isn't actually used by the function
|
||||
self.context_factory.creatorForNetloc(
|
||||
self.instance_map[worker_name].host,
|
||||
self.instance_map[worker_name].host.encode("utf-8"),
|
||||
self.instance_map[worker_name].port,
|
||||
),
|
||||
endpoint,
|
||||
|
|
|
@ -108,9 +108,12 @@ def return_json_error(
|
|||
|
||||
if f.check(SynapseError):
|
||||
# mypy doesn't understand that f.check asserts the type.
|
||||
exc: SynapseError = f.value # type: ignore
|
||||
exc: SynapseError = f.value
|
||||
error_code = exc.code
|
||||
error_dict = exc.error_dict(config)
|
||||
if exc.headers is not None:
|
||||
for header, value in exc.headers.items():
|
||||
request.setHeader(header, value)
|
||||
logger.info("%s SynapseError: %s - %s", request, error_code, exc.msg)
|
||||
elif f.check(CancelledError):
|
||||
error_code = HTTP_STATUS_REQUEST_CANCELLED
|
||||
|
@ -121,7 +124,7 @@ def return_json_error(
|
|||
"Got cancellation before client disconnection from %r: %r",
|
||||
request.request_metrics.name,
|
||||
request,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
else:
|
||||
error_code = 500
|
||||
|
@ -131,7 +134,7 @@ def return_json_error(
|
|||
"Failed handle request via %r: %r",
|
||||
request.request_metrics.name,
|
||||
request,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
|
||||
# Only respond with an error response if we haven't already started writing,
|
||||
|
@ -169,9 +172,12 @@ def return_html_error(
|
|||
"""
|
||||
if f.check(CodeMessageException):
|
||||
# mypy doesn't understand that f.check asserts the type.
|
||||
cme: CodeMessageException = f.value # type: ignore
|
||||
cme: CodeMessageException = f.value
|
||||
code = cme.code
|
||||
msg = cme.msg
|
||||
if cme.headers is not None:
|
||||
for header, value in cme.headers.items():
|
||||
request.setHeader(header, value)
|
||||
|
||||
if isinstance(cme, RedirectException):
|
||||
logger.info("%s redirect to %s", request, cme.location)
|
||||
|
@ -183,7 +189,7 @@ def return_html_error(
|
|||
logger.error(
|
||||
"Failed handle request %r",
|
||||
request,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
elif f.check(CancelledError):
|
||||
code = HTTP_STATUS_REQUEST_CANCELLED
|
||||
|
@ -193,7 +199,7 @@ def return_html_error(
|
|||
logger.error(
|
||||
"Got cancellation before client disconnection when handling request %r",
|
||||
request,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
else:
|
||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
@ -202,7 +208,7 @@ def return_html_error(
|
|||
logger.error(
|
||||
"Failed handle request %r",
|
||||
request,
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
||||
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||
)
|
||||
|
||||
if isinstance(error_template, str):
|
||||
|
|
|
@ -171,6 +171,7 @@ from functools import wraps
|
|||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
ContextManager,
|
||||
|
@ -903,6 +904,7 @@ def _custom_sync_async_decorator(
|
|||
"""
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
# For this branch, we handle async functions like `async def func() -> RInner`.
|
||||
# In this branch, R = Awaitable[RInner], for some other type RInner
|
||||
@wraps(func)
|
||||
async def _wrapper(
|
||||
|
@ -914,15 +916,16 @@ def _custom_sync_async_decorator(
|
|||
return await func(*args, **kwargs) # type: ignore[misc]
|
||||
|
||||
else:
|
||||
# The other case here handles both sync functions and those
|
||||
# decorated with inlineDeferred.
|
||||
# The other case here handles sync functions including those decorated with
|
||||
# `@defer.inlineCallbacks` or that return a `Deferred` or other `Awaitable`.
|
||||
@wraps(func)
|
||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
|
||||
scope = wrapping_logic(func, *args, **kwargs)
|
||||
scope.__enter__()
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
if isinstance(result, defer.Deferred):
|
||||
|
||||
def call_back(result: R) -> R:
|
||||
|
@ -930,20 +933,32 @@ def _custom_sync_async_decorator(
|
|||
return result
|
||||
|
||||
def err_back(result: R) -> R:
|
||||
# TODO: Pass the error details into `scope.__exit__(...)` for
|
||||
# consistency with the other paths.
|
||||
scope.__exit__(None, None, None)
|
||||
return result
|
||||
|
||||
result.addCallbacks(call_back, err_back)
|
||||
|
||||
else:
|
||||
if inspect.isawaitable(result):
|
||||
logger.error(
|
||||
"@trace may not have wrapped %s correctly! "
|
||||
"The function is not async but returned a %s.",
|
||||
func.__qualname__,
|
||||
type(result).__name__,
|
||||
)
|
||||
elif inspect.isawaitable(result):
|
||||
|
||||
async def wrap_awaitable() -> Any:
|
||||
try:
|
||||
assert isinstance(result, Awaitable)
|
||||
awaited_result = await result
|
||||
scope.__exit__(None, None, None)
|
||||
return awaited_result
|
||||
except Exception as e:
|
||||
scope.__exit__(type(e), None, e.__traceback__)
|
||||
raise
|
||||
|
||||
# The original method returned an awaitable, eg. a coroutine, so we
|
||||
# create another awaitable wrapping it that calls
|
||||
# `scope.__exit__(...)`.
|
||||
return wrap_awaitable()
|
||||
else:
|
||||
# Just a simple sync function so we can just exit the scope and
|
||||
# return the result without any fuss.
|
||||
scope.__exit__(None, None, None)
|
||||
|
||||
return result
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
import html
|
||||
import logging
|
||||
import urllib.parse
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, cast
|
||||
|
||||
import attr
|
||||
|
||||
|
@ -98,7 +98,7 @@ class OEmbedProvider:
|
|||
# No match.
|
||||
return None
|
||||
|
||||
def autodiscover_from_html(self, tree: "etree.Element") -> Optional[str]:
|
||||
def autodiscover_from_html(self, tree: "etree._Element") -> Optional[str]:
|
||||
"""
|
||||
Search an HTML document for oEmbed autodiscovery information.
|
||||
|
||||
|
@ -109,18 +109,22 @@ class OEmbedProvider:
|
|||
The URL to use for oEmbed information, or None if no URL was found.
|
||||
"""
|
||||
# Search for link elements with the proper rel and type attributes.
|
||||
for tag in tree.xpath(
|
||||
"//link[@rel='alternate'][@type='application/json+oembed']"
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
for tag in cast(
|
||||
List["etree._Element"],
|
||||
tree.xpath("//link[@rel='alternate'][@type='application/json+oembed']"),
|
||||
):
|
||||
if "href" in tag.attrib:
|
||||
return tag.attrib["href"]
|
||||
return cast(str, tag.attrib["href"])
|
||||
|
||||
# Some providers (e.g. Flickr) use alternative instead of alternate.
|
||||
for tag in tree.xpath(
|
||||
"//link[@rel='alternative'][@type='application/json+oembed']"
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
for tag in cast(
|
||||
List["etree._Element"],
|
||||
tree.xpath("//link[@rel='alternative'][@type='application/json+oembed']"),
|
||||
):
|
||||
if "href" in tag.attrib:
|
||||
return tag.attrib["href"]
|
||||
return cast(str, tag.attrib["href"])
|
||||
|
||||
return None
|
||||
|
||||
|
@ -212,11 +216,12 @@ class OEmbedProvider:
|
|||
return OEmbedResult(open_graph_response, author_name, cache_age)
|
||||
|
||||
|
||||
def _fetch_urls(tree: "etree.Element", tag_name: str) -> List[str]:
|
||||
def _fetch_urls(tree: "etree._Element", tag_name: str) -> List[str]:
|
||||
results = []
|
||||
for tag in tree.xpath("//*/" + tag_name):
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
for tag in cast(List["etree._Element"], tree.xpath("//*/" + tag_name)):
|
||||
if "src" in tag.attrib:
|
||||
results.append(tag.attrib["src"])
|
||||
results.append(cast(str, tag.attrib["src"]))
|
||||
return results
|
||||
|
||||
|
||||
|
@ -244,11 +249,12 @@ def calc_description_and_urls(open_graph_response: JsonDict, html_body: str) ->
|
|||
parser = etree.HTMLParser(recover=True, encoding="utf-8")
|
||||
|
||||
# Attempt to parse the body. If this fails, log and return no metadata.
|
||||
tree = etree.fromstring(html_body, parser)
|
||||
# TODO Develop of lxml-stubs has this correct.
|
||||
tree = etree.fromstring(html_body, parser) # type: ignore[arg-type]
|
||||
|
||||
# The data was successfully parsed, but no tree was found.
|
||||
if tree is None:
|
||||
return
|
||||
return # type: ignore[unreachable]
|
||||
|
||||
# Attempt to find interesting URLs (images, videos, embeds).
|
||||
if "og:image" not in open_graph_response:
|
||||
|
|
|
@ -24,6 +24,7 @@ from typing import (
|
|||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -115,7 +116,7 @@ def _get_html_media_encodings(
|
|||
|
||||
def decode_body(
|
||||
body: bytes, uri: str, content_type: Optional[str] = None
|
||||
) -> Optional["etree.Element"]:
|
||||
) -> Optional["etree._Element"]:
|
||||
"""
|
||||
This uses lxml to parse the HTML document.
|
||||
|
||||
|
@ -152,11 +153,12 @@ def decode_body(
|
|||
|
||||
# Attempt to parse the body. Returns None if the body was successfully
|
||||
# parsed, but no tree was found.
|
||||
return etree.fromstring(body, parser)
|
||||
# TODO Develop of lxml-stubs has this correct.
|
||||
return etree.fromstring(body, parser) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_meta_tags(
|
||||
tree: "etree.Element",
|
||||
tree: "etree._Element",
|
||||
property: str,
|
||||
prefix: str,
|
||||
property_mapper: Optional[Callable[[str], Optional[str]]] = None,
|
||||
|
@ -175,9 +177,15 @@ def _get_meta_tags(
|
|||
Returns:
|
||||
A map of tag name to value.
|
||||
"""
|
||||
# This actually returns Dict[str, str], but the caller sets this as a variable
|
||||
# which is Dict[str, Optional[str]].
|
||||
results: Dict[str, Optional[str]] = {}
|
||||
for tag in tree.xpath(
|
||||
f"//*/meta[starts-with(@{property}, '{prefix}:')][@content][not(@content='')]"
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
for tag in cast(
|
||||
List["etree._Element"],
|
||||
tree.xpath(
|
||||
f"//*/meta[starts-with(@{property}, '{prefix}:')][@content][not(@content='')]"
|
||||
),
|
||||
):
|
||||
# if we've got more than 50 tags, someone is taking the piss
|
||||
if len(results) >= 50:
|
||||
|
@ -187,14 +195,15 @@ def _get_meta_tags(
|
|||
)
|
||||
return {}
|
||||
|
||||
key = tag.attrib[property]
|
||||
key = cast(str, tag.attrib[property])
|
||||
if property_mapper:
|
||||
key = property_mapper(key)
|
||||
new_key = property_mapper(key)
|
||||
# None is a special value used to ignore a value.
|
||||
if key is None:
|
||||
if new_key is None:
|
||||
continue
|
||||
key = new_key
|
||||
|
||||
results[key] = tag.attrib["content"]
|
||||
results[key] = cast(str, tag.attrib["content"])
|
||||
|
||||
return results
|
||||
|
||||
|
@ -219,7 +228,7 @@ def _map_twitter_to_open_graph(key: str) -> Optional[str]:
|
|||
return "og" + key[7:]
|
||||
|
||||
|
||||
def parse_html_to_open_graph(tree: "etree.Element") -> Dict[str, Optional[str]]:
|
||||
def parse_html_to_open_graph(tree: "etree._Element") -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Parse the HTML document into an Open Graph response.
|
||||
|
||||
|
@ -276,24 +285,36 @@ def parse_html_to_open_graph(tree: "etree.Element") -> Dict[str, Optional[str]]:
|
|||
|
||||
if "og:title" not in og:
|
||||
# Attempt to find a title from the title tag, or the biggest header on the page.
|
||||
title = tree.xpath("((//title)[1] | (//h1)[1] | (//h2)[1] | (//h3)[1])/text()")
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
title = cast(
|
||||
List["etree._ElementUnicodeResult"],
|
||||
tree.xpath("((//title)[1] | (//h1)[1] | (//h2)[1] | (//h3)[1])/text()"),
|
||||
)
|
||||
if title:
|
||||
og["og:title"] = title[0].strip()
|
||||
else:
|
||||
og["og:title"] = None
|
||||
|
||||
if "og:image" not in og:
|
||||
meta_image = tree.xpath(
|
||||
"//*/meta[translate(@itemprop, 'IMAGE', 'image')='image'][not(@content='')]/@content[1]"
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
meta_image = cast(
|
||||
List["etree._ElementUnicodeResult"],
|
||||
tree.xpath(
|
||||
"//*/meta[translate(@itemprop, 'IMAGE', 'image')='image'][not(@content='')]/@content[1]"
|
||||
),
|
||||
)
|
||||
# If a meta image is found, use it.
|
||||
if meta_image:
|
||||
og["og:image"] = meta_image[0]
|
||||
else:
|
||||
# Try to find images which are larger than 10px by 10px.
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
#
|
||||
# TODO: consider inlined CSS styles as well as width & height attribs
|
||||
images = tree.xpath("//img[@src][number(@width)>10][number(@height)>10]")
|
||||
images = cast(
|
||||
List["etree._Element"],
|
||||
tree.xpath("//img[@src][number(@width)>10][number(@height)>10]"),
|
||||
)
|
||||
images = sorted(
|
||||
images,
|
||||
key=lambda i: (
|
||||
|
@ -302,20 +323,29 @@ def parse_html_to_open_graph(tree: "etree.Element") -> Dict[str, Optional[str]]:
|
|||
)
|
||||
# If no images were found, try to find *any* images.
|
||||
if not images:
|
||||
images = tree.xpath("//img[@src][1]")
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
images = cast(List["etree._Element"], tree.xpath("//img[@src][1]"))
|
||||
if images:
|
||||
og["og:image"] = images[0].attrib["src"]
|
||||
og["og:image"] = cast(str, images[0].attrib["src"])
|
||||
|
||||
# Finally, fallback to the favicon if nothing else.
|
||||
else:
|
||||
favicons = tree.xpath("//link[@href][contains(@rel, 'icon')]/@href[1]")
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
favicons = cast(
|
||||
List["etree._ElementUnicodeResult"],
|
||||
tree.xpath("//link[@href][contains(@rel, 'icon')]/@href[1]"),
|
||||
)
|
||||
if favicons:
|
||||
og["og:image"] = favicons[0]
|
||||
|
||||
if "og:description" not in og:
|
||||
# Check the first meta description tag for content.
|
||||
meta_description = tree.xpath(
|
||||
"//*/meta[translate(@name, 'DESCRIPTION', 'description')='description'][not(@content='')]/@content[1]"
|
||||
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||
meta_description = cast(
|
||||
List["etree._ElementUnicodeResult"],
|
||||
tree.xpath(
|
||||
"//*/meta[translate(@name, 'DESCRIPTION', 'description')='description'][not(@content='')]/@content[1]"
|
||||
),
|
||||
)
|
||||
# If a meta description is found with content, use it.
|
||||
if meta_description:
|
||||
|
@ -332,7 +362,7 @@ def parse_html_to_open_graph(tree: "etree.Element") -> Dict[str, Optional[str]]:
|
|||
return og
|
||||
|
||||
|
||||
def parse_html_description(tree: "etree.Element") -> Optional[str]:
|
||||
def parse_html_description(tree: "etree._Element") -> Optional[str]:
|
||||
"""
|
||||
Calculate a text description based on an HTML document.
|
||||
|
||||
|
@ -368,6 +398,9 @@ def parse_html_description(tree: "etree.Element") -> Optional[str]:
|
|||
"canvas",
|
||||
"img",
|
||||
"picture",
|
||||
# etree.Comment is a function which creates an etree._Comment element.
|
||||
# The "tag" attribute of an etree._Comment instance is confusingly the
|
||||
# etree.Comment function instead of a string.
|
||||
etree.Comment,
|
||||
}
|
||||
|
||||
|
@ -381,8 +414,8 @@ def parse_html_description(tree: "etree.Element") -> Optional[str]:
|
|||
|
||||
|
||||
def _iterate_over_text(
|
||||
tree: Optional["etree.Element"],
|
||||
tags_to_ignore: Set[Union[str, "etree.Comment"]],
|
||||
tree: Optional["etree._Element"],
|
||||
tags_to_ignore: Set[object],
|
||||
stack_limit: int = 1024,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Iterate over the tree returning text nodes in a depth first fashion,
|
||||
|
@ -402,7 +435,7 @@ def _iterate_over_text(
|
|||
|
||||
# This is a stack whose items are elements to iterate over *or* strings
|
||||
# to be returned.
|
||||
elements: List[Union[str, "etree.Element"]] = [tree]
|
||||
elements: List[Union[str, "etree._Element"]] = [tree]
|
||||
while elements:
|
||||
el = elements.pop()
|
||||
|
||||
|
|
|
@ -77,6 +77,8 @@ RegistryProxy = cast(CollectorRegistry, _RegistryProxy)
|
|||
|
||||
@attr.s(slots=True, hash=True, auto_attribs=True)
|
||||
class LaterGauge(Collector):
|
||||
"""A Gauge which periodically calls a user-provided callback to produce metrics."""
|
||||
|
||||
name: str
|
||||
desc: str
|
||||
labels: Optional[Sequence[str]] = attr.ib(hash=False)
|
||||
|
|
|
@ -38,6 +38,7 @@ from twisted.web.resource import Resource
|
|||
|
||||
from synapse.api import errors
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.config import ConfigError
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.presence_router import (
|
||||
GET_INTERESTED_USERS_CALLBACK,
|
||||
|
@ -121,6 +122,7 @@ from synapse.types import (
|
|||
JsonMapping,
|
||||
Requester,
|
||||
RoomAlias,
|
||||
RoomID,
|
||||
StateMap,
|
||||
UserID,
|
||||
UserInfo,
|
||||
|
@ -252,6 +254,7 @@ class ModuleApi:
|
|||
self._device_handler = hs.get_device_handler()
|
||||
self.custom_template_dir = hs.config.server.custom_template_directory
|
||||
self._callbacks = hs.get_module_api_callbacks()
|
||||
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||
|
||||
try:
|
||||
app_name = self._hs.config.email.email_app_name
|
||||
|
@ -419,6 +422,11 @@ class ModuleApi:
|
|||
|
||||
Added in Synapse v1.46.0.
|
||||
"""
|
||||
if self.msc3861_oauth_delegation_enabled:
|
||||
raise ConfigError(
|
||||
"Cannot use password auth provider callbacks when OAuth delegation is enabled"
|
||||
)
|
||||
|
||||
return self._password_auth_provider.register_password_auth_provider_callbacks(
|
||||
check_3pid_auth=check_3pid_auth,
|
||||
on_logged_out=on_logged_out,
|
||||
|
@ -647,7 +655,9 @@ class ModuleApi:
|
|||
Returns:
|
||||
The profile information (i.e. display name and avatar URL).
|
||||
"""
|
||||
return await self._store.get_profileinfo(localpart)
|
||||
server_name = self._hs.hostname
|
||||
user_id = UserID.from_string(f"@{localpart}:{server_name}")
|
||||
return await self._store.get_profileinfo(user_id)
|
||||
|
||||
async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]:
|
||||
"""Look up the threepids (email addresses and phone numbers) associated with the
|
||||
|
@ -1563,6 +1573,32 @@ class ModuleApi:
|
|||
start_timestamp, end_timestamp
|
||||
)
|
||||
|
||||
async def get_canonical_room_alias(self, room_id: RoomID) -> Optional[RoomAlias]:
|
||||
"""
|
||||
Retrieve the given room's current canonical alias.
|
||||
|
||||
A room may declare an alias as "canonical", meaning that it is the
|
||||
preferred alias to use when referring to the room. This function
|
||||
retrieves that alias from the room's state.
|
||||
|
||||
Added in Synapse v1.86.0.
|
||||
|
||||
Args:
|
||||
room_id: The Room ID to find the alias of.
|
||||
|
||||
Returns:
|
||||
None if the room ID does not exist, or if the room exists but has no canonical alias.
|
||||
Otherwise, the parsed room alias.
|
||||
"""
|
||||
room_alias_str = (
|
||||
await self._storage_controllers.state.get_canonical_alias_for_room(
|
||||
room_id.to_string()
|
||||
)
|
||||
)
|
||||
if room_alias_str:
|
||||
return RoomAlias.from_string(room_alias_str)
|
||||
return None
|
||||
|
||||
async def lookup_room_alias(self, room_alias: str) -> Tuple[str, List[str]]:
|
||||
"""
|
||||
Get the room ID associated with a room alias.
|
||||
|
|
|
@ -120,9 +120,6 @@ class BulkPushRuleEvaluator:
|
|||
self.should_calculate_push_rules = self.hs.config.push.enable_push
|
||||
|
||||
self._related_event_match_enabled = self.hs.config.experimental.msc3664_enabled
|
||||
self._intentional_mentions_enabled = (
|
||||
self.hs.config.experimental.msc3952_intentional_mentions
|
||||
)
|
||||
|
||||
self.room_push_rule_cache_metrics = register_cache(
|
||||
"cache",
|
||||
|
@ -390,10 +387,7 @@ class BulkPushRuleEvaluator:
|
|||
del notification_levels[key]
|
||||
|
||||
# Pull out any user and room mentions.
|
||||
has_mentions = (
|
||||
self._intentional_mentions_enabled
|
||||
and EventContentFields.MSC3952_MENTIONS in event.content
|
||||
)
|
||||
has_mentions = EventContentFields.MENTIONS in event.content
|
||||
|
||||
evaluator = PushRuleEvaluator(
|
||||
_flatten_dict(event),
|
||||
|
|
|
@ -247,7 +247,7 @@ class Mailer:
|
|||
|
||||
try:
|
||||
user_display_name = await self.store.get_profile_displayname(
|
||||
UserID.from_string(user_id).localpart
|
||||
UserID.from_string(user_id)
|
||||
)
|
||||
if user_display_name is None:
|
||||
user_display_name = user_id
|
||||
|
|
|
@ -257,9 +257,11 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||
DeleteRoomStatusByRoomIdRestServlet(hs).register(http_server)
|
||||
JoinRoomAliasServlet(hs).register(http_server)
|
||||
VersionServlet(hs).register(http_server)
|
||||
UserAdminServlet(hs).register(http_server)
|
||||
if not hs.config.experimental.msc3861.enabled:
|
||||
UserAdminServlet(hs).register(http_server)
|
||||
UserMembershipRestServlet(hs).register(http_server)
|
||||
UserTokenRestServlet(hs).register(http_server)
|
||||
if not hs.config.experimental.msc3861.enabled:
|
||||
UserTokenRestServlet(hs).register(http_server)
|
||||
UserRestServletV2(hs).register(http_server)
|
||||
UsersRestServletV2(hs).register(http_server)
|
||||
UserMediaStatisticsRestServlet(hs).register(http_server)
|
||||
|
@ -274,9 +276,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||
RoomEventContextServlet(hs).register(http_server)
|
||||
RateLimitRestServlet(hs).register(http_server)
|
||||
UsernameAvailableRestServlet(hs).register(http_server)
|
||||
ListRegistrationTokensRestServlet(hs).register(http_server)
|
||||
NewRegistrationTokenRestServlet(hs).register(http_server)
|
||||
RegistrationTokenRestServlet(hs).register(http_server)
|
||||
if not hs.config.experimental.msc3861.enabled:
|
||||
ListRegistrationTokensRestServlet(hs).register(http_server)
|
||||
NewRegistrationTokenRestServlet(hs).register(http_server)
|
||||
RegistrationTokenRestServlet(hs).register(http_server)
|
||||
DestinationMembershipRestServlet(hs).register(http_server)
|
||||
DestinationResetConnectionRestServlet(hs).register(http_server)
|
||||
DestinationRestServlet(hs).register(http_server)
|
||||
|
@ -306,10 +309,12 @@ def register_servlets_for_client_rest_resource(
|
|||
# The following resources can only be run on the main process.
|
||||
if hs.config.worker.worker_app is None:
|
||||
DeactivateAccountRestServlet(hs).register(http_server)
|
||||
ResetPasswordRestServlet(hs).register(http_server)
|
||||
if not hs.config.experimental.msc3861.enabled:
|
||||
ResetPasswordRestServlet(hs).register(http_server)
|
||||
SearchUsersRestServlet(hs).register(http_server)
|
||||
UserRegisterServlet(hs).register(http_server)
|
||||
AccountValidityRenewServlet(hs).register(http_server)
|
||||
if not hs.config.experimental.msc3861.enabled:
|
||||
UserRegisterServlet(hs).register(http_server)
|
||||
AccountValidityRenewServlet(hs).register(http_server)
|
||||
|
||||
# Load the media repo ones if we're using them. Otherwise load the servlets which
|
||||
# don't need a media repo (typically readonly admin APIs).
|
||||
|
|
|
@ -71,6 +71,7 @@ class UsersRestServletV2(RestServlet):
|
|||
self.auth = hs.get_auth()
|
||||
self.admin_handler = hs.get_admin_handler()
|
||||
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
|
||||
self._msc3861_enabled = hs.config.experimental.msc3861.enabled
|
||||
|
||||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
await assert_requester_is_admin(self.auth, request)
|
||||
|
@ -94,7 +95,14 @@ class UsersRestServletV2(RestServlet):
|
|||
|
||||
user_id = parse_string(request, "user_id")
|
||||
name = parse_string(request, "name")
|
||||
|
||||
guests = parse_boolean(request, "guests", default=True)
|
||||
if self._msc3861_enabled and guests:
|
||||
raise SynapseError(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"The guests parameter is not supported when MSC3861 is enabled.",
|
||||
errcode=Codes.INVALID_PARAM,
|
||||
)
|
||||
deactivated = parse_boolean(request, "deactivated", default=False)
|
||||
|
||||
# If support for MSC3866 is not enabled, apply no filtering based on the
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue