Merge branch 'release-v1.86' into matrix-org-hotfixes
commit
98a00339a5
|
@ -22,7 +22,21 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
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:
|
mypy:
|
||||||
|
needs: check_repo
|
||||||
|
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -47,6 +61,8 @@ jobs:
|
||||||
run: sed '/warn_unused_ignores = True/d' -i mypy.ini
|
run: sed '/warn_unused_ignores = True/d' -i mypy.ini
|
||||||
- run: poetry run mypy
|
- run: poetry run mypy
|
||||||
trial:
|
trial:
|
||||||
|
needs: check_repo
|
||||||
|
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -105,6 +121,8 @@ jobs:
|
||||||
|
|
||||||
|
|
||||||
sytest:
|
sytest:
|
||||||
|
needs: check_repo
|
||||||
|
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: matrixdotorg/sytest-synapse:testing
|
image: matrixdotorg/sytest-synapse:testing
|
||||||
|
@ -156,7 +174,8 @@ jobs:
|
||||||
|
|
||||||
|
|
||||||
complement:
|
complement:
|
||||||
if: "${{ !failure() && !cancelled() }}"
|
needs: check_repo
|
||||||
|
if: "!failure() && !cancelled() && needs.check_repo.outputs.should_run_workflow == 'true'"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -192,7 +211,7 @@ jobs:
|
||||||
# Open an issue if the build fails, so we know about it.
|
# 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.
|
# Only do this if we're not experimenting with this action in a PR.
|
||||||
open-issue:
|
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:
|
needs:
|
||||||
# TODO: should mypy be included here? It feels more brittle than the others.
|
# TODO: should mypy be included here? It feels more brittle than the others.
|
||||||
- mypy
|
- mypy
|
||||||
|
|
|
@ -34,6 +34,7 @@ jobs:
|
||||||
- id: set-distros
|
- id: set-distros
|
||||||
run: |
|
run: |
|
||||||
# if we're running from a tag, get the full list of distros; otherwise just use debian:sid
|
# 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"]'
|
dists='["debian:sid"]'
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
dists=$(scripts-dev/build_debian_packages.py --show-dists-json)
|
dists=$(scripts-dev/build_debian_packages.py --show-dists-json)
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- uses: matrix-org/setup-python-poetry@v1
|
- uses: matrix-org/setup-python-poetry@v1
|
||||||
with:
|
with:
|
||||||
|
@ -92,6 +92,10 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Setup Poetry
|
- name: Setup Poetry
|
||||||
uses: matrix-org/setup-python-poetry@v1
|
uses: matrix-org/setup-python-poetry@v1
|
||||||
with:
|
with:
|
||||||
|
@ -103,10 +107,6 @@ jobs:
|
||||||
# To make CI green, err towards caution and install the project.
|
# To make CI green, err towards caution and install the project.
|
||||||
install-project: "true"
|
install-project: "true"
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
# Cribbed from
|
# Cribbed from
|
||||||
# https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17
|
# https://github.com/AustinScola/mypy-cache-github-action/blob/85ea4f2972abed39b33bd02c36e341b28ca59213/src/restore.ts#L10-L17
|
||||||
- name: Restore/persist mypy's cache
|
- name: Restore/persist mypy's cache
|
||||||
|
@ -150,7 +150,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- uses: matrix-org/setup-python-poetry@v1
|
- uses: matrix-org/setup-python-poetry@v1
|
||||||
with:
|
with:
|
||||||
|
@ -167,7 +167,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
@ -268,7 +268,7 @@ jobs:
|
||||||
postgres:${{ matrix.job.postgres-version }}
|
postgres:${{ matrix.job.postgres-version }}
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- uses: matrix-org/setup-python-poetry@v1
|
- uses: matrix-org/setup-python-poetry@v1
|
||||||
|
@ -308,7 +308,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
# There aren't wheels for some of the older deps, so we need to install
|
# 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
|
run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Run SyTest
|
- name: Run SyTest
|
||||||
|
@ -556,7 +556,7 @@ jobs:
|
||||||
path: synapse
|
path: synapse
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
|
@ -584,7 +584,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.58.1
|
uses: dtolnay/rust-toolchain@1.60.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- run: cargo test
|
- run: cargo test
|
||||||
|
|
|
@ -18,7 +18,22 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
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:
|
mypy:
|
||||||
|
needs: check_repo
|
||||||
|
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -41,6 +56,8 @@ jobs:
|
||||||
- run: poetry run mypy
|
- run: poetry run mypy
|
||||||
|
|
||||||
trial:
|
trial:
|
||||||
|
needs: check_repo
|
||||||
|
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -75,6 +92,8 @@ jobs:
|
||||||
|| true
|
|| true
|
||||||
|
|
||||||
sytest:
|
sytest:
|
||||||
|
needs: check_repo
|
||||||
|
if: needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: matrixdotorg/sytest-synapse:buster
|
image: matrixdotorg/sytest-synapse:buster
|
||||||
|
@ -119,7 +138,8 @@ jobs:
|
||||||
/logs/**/*.log*
|
/logs/**/*.log*
|
||||||
|
|
||||||
complement:
|
complement:
|
||||||
if: "${{ !failure() && !cancelled() }}"
|
needs: check_repo
|
||||||
|
if: "!failure() && !cancelled() && needs.check_repo.outputs.should_run_workflow == 'true'"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -166,7 +186,7 @@ jobs:
|
||||||
|
|
||||||
# open an issue if the build fails, so we know about it.
|
# open an issue if the build fails, so we know about it.
|
||||||
open-issue:
|
open-issue:
|
||||||
if: failure()
|
if: failure() && needs.check_repo.outputs.should_run_workflow == 'true'
|
||||||
needs:
|
needs:
|
||||||
- mypy
|
- mypy
|
||||||
- trial
|
- 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)
|
Synapse 1.85.0rc1 (2023-05-30)
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.19"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
|
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
@ -132,9 +132,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.18"
|
version = "0.4.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
|
@ -229,9 +229,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-log"
|
name = "pyo3-log"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9c8b57fe71fb5dcf38970ebedc2b1531cf1c14b1b9b4c560a182a57e115575c"
|
checksum = "c94ff6535a6bae58d7d0b85e60d4c53f7f84d0d0aa35d6a28c3f3e70bfe51444"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"log",
|
"log",
|
||||||
|
@ -291,9 +291,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.7.3"
|
version = "1.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
|
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -302,9 +302,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.29"
|
version = "0.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
|
@ -320,18 +320,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.163"
|
version = "1.0.164"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.163"
|
version = "1.0.164"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"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
|
matrix-synapse-py3 (1.85.0~rc1) stable; urgency=medium
|
||||||
|
|
||||||
* New Synapse release 1.85.0rc1.
|
* 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
|
# We hardcode the use of Debian bullseye here because this could change upstream
|
||||||
# and other Dockerfiles used for testing are expecting bullseye.
|
# 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
|
# 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.
|
# 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
|
### 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
|
# install the OS build deps
|
||||||
RUN \
|
RUN \
|
||||||
|
@ -158,7 +158,7 @@ RUN --mount=type=cache,target=/synapse/target,sharing=locked \
|
||||||
### Stage 2: runtime
|
### 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.url='https://matrix.org/docs/projects/server/synapse'
|
||||||
LABEL org.opencontainers.image.documentation='https://github.com/matrix-org/synapse/blob/master/docker/README.md'
|
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
|
# https://launchpad.net/~jyrki-pulliainen/+archive/ubuntu/dh-virtualenv, but
|
||||||
# it's not obviously easier to use that than to build our own.)
|
# 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 apt-get update -qq -o Acquire::Languages=none
|
||||||
RUN env DEBIAN_FRONTEND=noninteractive apt-get install \
|
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
|
### Stage 1
|
||||||
###
|
###
|
||||||
FROM ${distro}
|
FROM docker.io/library/${distro}
|
||||||
|
|
||||||
# Get the distro we want to pull from as a dynamic build variable
|
# Get the distro we want to pull from as a dynamic build variable
|
||||||
# (We need to define it in each build stage)
|
# (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
|
# target image. For repeated rebuilds, this is much faster than apt installing
|
||||||
# each time.
|
# each time.
|
||||||
|
|
||||||
FROM debian:bullseye-slim AS deps_base
|
FROM docker.io/library/debian:bullseye-slim AS deps_base
|
||||||
RUN \
|
RUN \
|
||||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
--mount=type=cache,target=/var/lib/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
|
# 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
|
# based on the same debian version as the synapse image, to make sure we get
|
||||||
# the expected version of libc.
|
# 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
|
# now build the final image, based on the the regular Synapse docker image
|
||||||
FROM $FROM
|
FROM $FROM
|
||||||
|
|
|
@ -73,7 +73,8 @@ The following environment variables are supported in `generate` mode:
|
||||||
will log sensitive information such as access tokens.
|
will log sensitive information such as access tokens.
|
||||||
This should not be needed unless you are a developer attempting to debug something
|
This should not be needed unless you are a developer attempting to debug something
|
||||||
particularly tricky.
|
particularly tricky.
|
||||||
|
* `SYNAPSE_LOG_TESTING`: if set, Synapse will log additional information useful
|
||||||
|
for testing.
|
||||||
|
|
||||||
## Postgres
|
## 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
|
# 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
|
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
|
ARG FROM=matrixdotorg/synapse-workers:$SYNAPSE_VERSION
|
||||||
|
|
||||||
FROM $FROM
|
FROM $FROM
|
||||||
|
@ -19,8 +20,8 @@ FROM $FROM
|
||||||
# the same debian version as Synapse's docker image (so the versions of the
|
# the same debian version as Synapse's docker image (so the versions of the
|
||||||
# shared libraries match).
|
# shared libraries match).
|
||||||
RUN adduser --system --uid 999 postgres --home /var/lib/postgresql
|
RUN adduser --system --uid 999 postgres --home /var/lib/postgresql
|
||||||
COPY --from=postgres:13-bullseye /usr/lib/postgresql /usr/lib/postgresql
|
COPY --from=docker.io/library/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/share/postgresql /usr/share/postgresql
|
||||||
RUN mkdir /var/run/postgresql && chown postgres /var/run/postgresql
|
RUN mkdir /var/run/postgresql && chown postgres /var/run/postgresql
|
||||||
ENV PATH="${PATH}:/usr/lib/postgresql/13/bin"
|
ENV PATH="${PATH}:/usr/lib/postgresql/13/bin"
|
||||||
ENV PGDATA=/var/lib/postgresql/data
|
ENV PGDATA=/var/lib/postgresql/data
|
||||||
|
|
|
@ -49,17 +49,35 @@ handlers:
|
||||||
class: logging.StreamHandler
|
class: logging.StreamHandler
|
||||||
formatter: precise
|
formatter: precise
|
||||||
|
|
||||||
{% if not SYNAPSE_LOG_SENSITIVE %}
|
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
|
If SYNAPSE_LOG_SENSITIVE is unset, then override synapse.storage.SQL to INFO
|
||||||
so that DEBUG entries (containing sensitive information) are not emitted.
|
so that DEBUG entries (containing sensitive information) are not emitted.
|
||||||
#}
|
#}
|
||||||
loggers:
|
|
||||||
synapse.storage.SQL:
|
synapse.storage.SQL:
|
||||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||||
# information such as access tokens.
|
# information such as access tokens.
|
||||||
level: INFO
|
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:
|
root:
|
||||||
level: {{ SYNAPSE_LOG_LEVEL or "INFO" }}
|
level: {{ SYNAPSE_LOG_LEVEL or "INFO" }}
|
||||||
|
|
|
@ -40,6 +40,8 @@
|
||||||
# log level. INFO is the default.
|
# log level. INFO is the default.
|
||||||
# * SYNAPSE_LOG_SENSITIVE: If unset, SQL and SQL values won't be logged,
|
# * SYNAPSE_LOG_SENSITIVE: If unset, SQL and SQL values won't be logged,
|
||||||
# regardless of the SYNAPSE_LOG_LEVEL setting.
|
# 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
|
# 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
|
# 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(
|
extra_log_template_args["SYNAPSE_LOG_SENSITIVE"] = environ.get(
|
||||||
"SYNAPSE_LOG_SENSITIVE"
|
"SYNAPSE_LOG_SENSITIVE"
|
||||||
)
|
)
|
||||||
|
extra_log_template_args["SYNAPSE_LOG_TESTING"] = environ.get("SYNAPSE_LOG_TESTING")
|
||||||
|
|
||||||
# Render and write the file
|
# Render and write the file
|
||||||
log_config_filepath = f"/conf/workers/{worker_name}.log.config"
|
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
|
# We hardcode the use of Debian bullseye here because this could change upstream
|
||||||
# and other Dockerfiles used for testing are expecting bullseye.
|
# 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 Rust and other dependencies (stolen from normal Dockerfile)
|
||||||
# install the OS build deps
|
# 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
|
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
|
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
|
# Upgrading to v1.84.0
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,8 @@ What servers are currently participating in this room?
|
||||||
Run this sql query on your db:
|
Run this sql query on your db:
|
||||||
```sql
|
```sql
|
||||||
SELECT DISTINCT split_part(state_key, ':', 2)
|
SELECT DISTINCT split_part(state_key, ':', 2)
|
||||||
FROM current_state_events AS c
|
FROM current_state_events
|
||||||
INNER JOIN room_memberships AS m USING (room_id, event_id)
|
WHERE room_id = '!cURbafjkfsMDVwdRDQ:matrix.org' AND membership = 'join';
|
||||||
WHERE room_id = '!cURbafjkfsMDVwdRDQ:matrix.org' AND membership = 'join';
|
|
||||||
```
|
```
|
||||||
|
|
||||||
What users are registered on my server?
|
What users are registered on my server?
|
||||||
|
|
|
@ -1196,6 +1196,32 @@ Example configuration:
|
||||||
allow_device_name_lookup_over_federation: true
|
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
|
## Caching
|
||||||
|
|
||||||
Options related to caching.
|
Options related to caching.
|
||||||
|
@ -2570,7 +2596,50 @@ Example configuration:
|
||||||
```yaml
|
```yaml
|
||||||
nonrefreshable_access_token_lifetime: 24h
|
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
|
## Metrics
|
||||||
Config options related to metrics.
|
Config options related to metrics.
|
||||||
|
@ -3415,28 +3484,6 @@ password_config:
|
||||||
require_uppercase: true
|
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
|
## Push
|
||||||
Configuration settings related to push notifications
|
Configuration settings related to push notifications
|
||||||
|
|
||||||
|
|
26
mypy.ini
26
mypy.ini
|
@ -2,17 +2,29 @@
|
||||||
namespace_packages = True
|
namespace_packages = True
|
||||||
plugins = pydantic.mypy, mypy_zope:plugin, scripts-dev/mypy_synapse_plugin.py
|
plugins = pydantic.mypy, mypy_zope:plugin, scripts-dev/mypy_synapse_plugin.py
|
||||||
follow_imports = normal
|
follow_imports = normal
|
||||||
check_untyped_defs = True
|
|
||||||
show_error_codes = True
|
show_error_codes = True
|
||||||
show_traceback = True
|
show_traceback = True
|
||||||
mypy_path = stubs
|
mypy_path = stubs
|
||||||
warn_unreachable = True
|
warn_unreachable = True
|
||||||
warn_unused_ignores = True
|
|
||||||
local_partial_types = True
|
local_partial_types = True
|
||||||
no_implicit_optional = 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
|
disallow_untyped_defs = True
|
||||||
strict_equality = True
|
disallow_incomplete_defs = True
|
||||||
|
# check_untyped_defs = True
|
||||||
|
# disallow_untyped_decorators = True
|
||||||
warn_redundant_casts = 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
|
# Run mypy type checking with the minimum supported Python version to catch new usage
|
||||||
# that isn't backwards-compatible (types, overloads, etc).
|
# that isn't backwards-compatible (types, overloads, etc).
|
||||||
python_version = 3.8
|
python_version = 3.8
|
||||||
|
@ -31,6 +43,7 @@ warn_unused_ignores = False
|
||||||
|
|
||||||
[mypy-synapse.util.caches.treecache]
|
[mypy-synapse.util.caches.treecache]
|
||||||
disallow_untyped_defs = False
|
disallow_untyped_defs = False
|
||||||
|
disallow_incomplete_defs = False
|
||||||
|
|
||||||
;; Dependencies without annotations
|
;; Dependencies without annotations
|
||||||
;; Before ignoring a module, check to see if type stubs are available.
|
;; 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
|
;; which we can pull in as a dev dependency by adding to `pyproject.toml`'s
|
||||||
;; `[tool.poetry.dev-dependencies]` list.
|
;; `[tool.poetry.dev-dependencies]` list.
|
||||||
|
|
||||||
|
# https://github.com/lepture/authlib/issues/460
|
||||||
[mypy-authlib.*]
|
[mypy-authlib.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-ijson.*]
|
[mypy-ijson.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-lxml]
|
# https://github.com/msgpack/msgpack-python/issues/448
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-msgpack]
|
[mypy-msgpack]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
# https://github.com/wolever/parameterized/issues/143
|
||||||
[mypy-parameterized.*]
|
[mypy-parameterized.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
@ -73,6 +86,7 @@ ignore_missing_imports = True
|
||||||
[mypy-srvlookup.*]
|
[mypy-srvlookup.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
# https://github.com/twisted/treq/pull/366
|
||||||
[mypy-treq.*]
|
[mypy-treq.*]
|
||||||
ignore_missing_imports = True
|
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]
|
[tool.poetry]
|
||||||
name = "matrix-synapse"
|
name = "matrix-synapse"
|
||||||
version = "1.85.0rc1"
|
version = "1.85.2"
|
||||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
@ -314,6 +314,7 @@ black = ">=22.3.0"
|
||||||
ruff = "0.0.265"
|
ruff = "0.0.265"
|
||||||
|
|
||||||
# Typechecking
|
# Typechecking
|
||||||
|
lxml-stubs = ">=0.4.0"
|
||||||
mypy = "*"
|
mypy = "*"
|
||||||
mypy-zope = "*"
|
mypy-zope = "*"
|
||||||
types-bleach = ">=4.1.0"
|
types-bleach = ">=4.1.0"
|
||||||
|
|
|
@ -7,7 +7,7 @@ name = "synapse"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.58.1"
|
rust-version = "1.60.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "synapse"
|
name = "synapse"
|
||||||
|
|
|
@ -13,8 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
#![feature(test)]
|
#![feature(test)]
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use synapse::push::{
|
use synapse::push::{
|
||||||
evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, JsonValue,
|
evaluator::PushRuleEvaluator, Condition, EventMatchCondition, FilteredPushRules, JsonValue,
|
||||||
PushRules, SimpleJsonValue,
|
PushRules, SimpleJsonValue,
|
||||||
|
@ -197,7 +195,6 @@ fn bench_eval_message(b: &mut Bencher) {
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
b.iter(|| eval.run(&rules, Some("bob"), Some("person")));
|
b.iter(|| eval.run(&rules, Some("bob"), Some("person")));
|
||||||
|
|
|
@ -142,11 +142,11 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
||||||
default_enabled: true,
|
default_enabled: true,
|
||||||
},
|
},
|
||||||
PushRule {
|
PushRule {
|
||||||
rule_id: Cow::Borrowed(".org.matrix.msc3952.is_user_mention"),
|
rule_id: Cow::Borrowed("global/override/.m.is_user_mention"),
|
||||||
priority_class: 5,
|
priority_class: 5,
|
||||||
conditions: Cow::Borrowed(&[Condition::Known(
|
conditions: Cow::Borrowed(&[Condition::Known(
|
||||||
KnownCondition::ExactEventPropertyContainsType(EventPropertyIsTypeCondition {
|
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),
|
value_type: Cow::Borrowed(&EventMatchPatternType::UserId),
|
||||||
}),
|
}),
|
||||||
)]),
|
)]),
|
||||||
|
@ -163,11 +163,11 @@ pub const BASE_APPEND_OVERRIDE_RULES: &[PushRule] = &[
|
||||||
default_enabled: true,
|
default_enabled: true,
|
||||||
},
|
},
|
||||||
PushRule {
|
PushRule {
|
||||||
rule_id: Cow::Borrowed(".org.matrix.msc3952.is_room_mention"),
|
rule_id: Cow::Borrowed("global/override/.m.is_room_mention"),
|
||||||
priority_class: 5,
|
priority_class: 5,
|
||||||
conditions: Cow::Borrowed(&[
|
conditions: Cow::Borrowed(&[
|
||||||
Condition::Known(KnownCondition::EventPropertyIs(EventPropertyIsCondition {
|
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)),
|
value: Cow::Borrowed(&SimpleJsonValue::Bool(true)),
|
||||||
})),
|
})),
|
||||||
Condition::Known(KnownCondition::SenderNotificationPermission {
|
Condition::Known(KnownCondition::SenderNotificationPermission {
|
||||||
|
|
|
@ -70,7 +70,9 @@ pub struct PushRuleEvaluator {
|
||||||
/// The "content.body", if any.
|
/// The "content.body", if any.
|
||||||
body: String,
|
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,
|
has_mentions: bool,
|
||||||
|
|
||||||
/// The number of users in the room.
|
/// The number of users in the room.
|
||||||
|
@ -155,9 +157,7 @@ impl PushRuleEvaluator {
|
||||||
let rule_id = &push_rule.rule_id().to_string();
|
let rule_id = &push_rule.rule_id().to_string();
|
||||||
|
|
||||||
// For backwards-compatibility the legacy mention rules are disabled
|
// For backwards-compatibility the legacy mention rules are disabled
|
||||||
// if the event contains the 'm.mentions' property (and if the
|
// if the event contains the 'm.mentions' property.
|
||||||
// experimental feature is enabled, both of these are represented
|
|
||||||
// by the has_mentions flag).
|
|
||||||
if self.has_mentions
|
if self.has_mentions
|
||||||
&& (rule_id == "global/override/.m.rule.contains_display_name"
|
&& (rule_id == "global/override/.m.rule.contains_display_name"
|
||||||
|| rule_id == "global/content/.m.rule.contains_user_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]);
|
let rules = PushRules::new(vec![custom_rule]);
|
||||||
result = evaluator.run(
|
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,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
|
@ -527,7 +527,6 @@ pub struct FilteredPushRules {
|
||||||
msc1767_enabled: bool,
|
msc1767_enabled: bool,
|
||||||
msc3381_polls_enabled: bool,
|
msc3381_polls_enabled: bool,
|
||||||
msc3664_enabled: bool,
|
msc3664_enabled: bool,
|
||||||
msc3952_intentional_mentions: bool,
|
|
||||||
msc3958_suppress_edits_enabled: bool,
|
msc3958_suppress_edits_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,7 +539,6 @@ impl FilteredPushRules {
|
||||||
msc1767_enabled: bool,
|
msc1767_enabled: bool,
|
||||||
msc3381_polls_enabled: bool,
|
msc3381_polls_enabled: bool,
|
||||||
msc3664_enabled: bool,
|
msc3664_enabled: bool,
|
||||||
msc3952_intentional_mentions: bool,
|
|
||||||
msc3958_suppress_edits_enabled: bool,
|
msc3958_suppress_edits_enabled: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -549,7 +547,6 @@ impl FilteredPushRules {
|
||||||
msc1767_enabled,
|
msc1767_enabled,
|
||||||
msc3381_polls_enabled,
|
msc3381_polls_enabled,
|
||||||
msc3664_enabled,
|
msc3664_enabled,
|
||||||
msc3952_intentional_mentions,
|
|
||||||
msc3958_suppress_edits_enabled,
|
msc3958_suppress_edits_enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -587,10 +584,6 @@ impl FilteredPushRules {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.msc3952_intentional_mentions && rule.rule_id.contains("org.matrix.msc3952")
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if !self.msc3958_suppress_edits_enabled
|
if !self.msc3958_suppress_edits_enabled
|
||||||
&& rule.rule_id == "global/override/.com.beeper.suppress_edits"
|
&& rule.rule_id == "global/override/.com.beeper.suppress_edits"
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,8 @@ from concurrent.futures import ThreadPoolExecutor
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
from typing import Collection, Optional, Sequence, Set
|
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 = (
|
DISTS = (
|
||||||
"debian:buster", # oldstable: EOL 2022-08
|
"debian:buster", # oldstable: EOL 2022-08
|
||||||
"debian:bullseye",
|
"debian:bullseye",
|
||||||
|
|
|
@ -269,6 +269,10 @@ if [[ -n "$SYNAPSE_TEST_LOG_LEVEL" ]]; then
|
||||||
export PASS_SYNAPSE_LOG_SENSITIVE=1
|
export PASS_SYNAPSE_LOG_SENSITIVE=1
|
||||||
fi
|
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!
|
# Run the tests!
|
||||||
echo "Images built; running complement"
|
echo "Images built; running complement"
|
||||||
cd "$COMPLEMENT_DIR"
|
cd "$COMPLEMENT_DIR"
|
||||||
|
|
|
@ -46,7 +46,6 @@ class FilteredPushRules:
|
||||||
msc1767_enabled: bool,
|
msc1767_enabled: bool,
|
||||||
msc3381_polls_enabled: bool,
|
msc3381_polls_enabled: bool,
|
||||||
msc3664_enabled: bool,
|
msc3664_enabled: bool,
|
||||||
msc3952_intentional_mentions: bool,
|
|
||||||
msc3958_suppress_edits_enabled: bool,
|
msc3958_suppress_edits_enabled: bool,
|
||||||
): ...
|
): ...
|
||||||
def rules(self) -> Collection[Tuple[PushRule, 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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,6 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
import pymacaroons
|
|
||||||
from netaddr import IPAddress
|
from netaddr import IPAddress
|
||||||
|
|
||||||
from twisted.web.server import Request
|
from twisted.web.server import Request
|
||||||
|
@ -24,19 +23,11 @@ from synapse.api.constants import EventTypes, HistoryVisibility, Membership
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
AuthError,
|
AuthError,
|
||||||
Codes,
|
Codes,
|
||||||
InvalidClientTokenError,
|
|
||||||
MissingClientTokenError,
|
MissingClientTokenError,
|
||||||
UnstableSpecAuthError,
|
UnstableSpecAuthError,
|
||||||
)
|
)
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
from synapse.http import get_request_user_agent
|
from synapse.logging.opentracing import trace
|
||||||
from synapse.http.site import SynapseRequest
|
|
||||||
from synapse.logging.opentracing import (
|
|
||||||
active_span,
|
|
||||||
force_tracing,
|
|
||||||
start_active_span,
|
|
||||||
trace,
|
|
||||||
)
|
|
||||||
from synapse.types import Requester, create_requester
|
from synapse.types import Requester, create_requester
|
||||||
from synapse.util.cancellation import cancellable
|
from synapse.util.cancellation import cancellable
|
||||||
|
|
||||||
|
@ -46,26 +37,13 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# guests always get this device id.
|
class BaseAuth:
|
||||||
GUEST_DEVICE_ID = "guest_device"
|
"""Common base class for all auth implementations."""
|
||||||
|
|
||||||
|
|
||||||
class Auth:
|
|
||||||
"""
|
|
||||||
This class contains functions for authenticating users of our client-server API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.clock = hs.get_clock()
|
|
||||||
self.store = hs.get_datastores().main
|
self.store = hs.get_datastores().main
|
||||||
self._account_validity_handler = hs.get_account_validity_handler()
|
|
||||||
self._storage_controllers = hs.get_storage_controllers()
|
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(
|
async def check_user_in_room(
|
||||||
self,
|
self,
|
||||||
|
@ -119,140 +97,50 @@ class Auth:
|
||||||
errcode=Codes.NOT_JOINED,
|
errcode=Codes.NOT_JOINED,
|
||||||
)
|
)
|
||||||
|
|
||||||
@cancellable
|
@trace
|
||||||
async def get_user_by_req(
|
async def check_user_in_room_or_world_readable(
|
||||||
self,
|
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
||||||
request: SynapseRequest,
|
) -> Tuple[str, Optional[str]]:
|
||||||
allow_guest: bool = False,
|
"""Checks that the user is or was in the room or the room is world
|
||||||
allow_expired: bool = False,
|
readable. If it isn't then an exception is raised.
|
||||||
) -> Requester:
|
|
||||||
"""Get a registered user's ID.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: An HTTP request with an access_token query parameter.
|
room_id: room to check
|
||||||
allow_guest: If False, will raise an AuthError if the user making the
|
user_id: user to check
|
||||||
request is a guest.
|
allow_departed_users: if True, accept users that were previously
|
||||||
allow_expired: If True, allow the request through even if the account
|
members but have now departed
|
||||||
is expired, or session token lifetime has ended. Note that
|
|
||||||
/login will deliver access tokens regardless of expiration.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Resolves to the requester
|
Resolves to the current membership of the user in the room and the
|
||||||
Raises:
|
membership event ID of the user. If the user is not in the room and
|
||||||
InvalidClientCredentialsError if no user by that token exists or the token
|
never has been, then `(Membership.JOIN, None)` is returned.
|
||||||
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:
|
try:
|
||||||
ip_addr = request.getClientAddress().host
|
# check_user_in_room will return the most recent membership
|
||||||
user_agent = get_request_user_agent(request)
|
# event for the user if:
|
||||||
|
# * The user is a non-guest user, and was ever in the room
|
||||||
access_token = self.get_access_token_from_request(request)
|
# * The user is a guest user, and has joined the room
|
||||||
|
# else it will throw.
|
||||||
# First check if it could be a request from an appservice
|
return await self.check_user_in_room(
|
||||||
requester = await self._get_appservice_user(request)
|
room_id, requester, allow_departed_users=allow_departed_users
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
except AuthError:
|
||||||
# Deny the request if the user account has expired.
|
visibility = await self._storage_controllers.state.get_current_state_event(
|
||||||
# This check is only done for regular users, not appservice ones.
|
room_id, EventTypes.RoomHistoryVisibility, ""
|
||||||
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 (
|
if (
|
||||||
requester.user.to_string() != requester.authenticated_entity
|
visibility
|
||||||
and self._track_puppeted_user_ips
|
and visibility.content.get("history_visibility")
|
||||||
|
== HistoryVisibility.WORLD_READABLE
|
||||||
):
|
):
|
||||||
await self.store.insert_client_ip(
|
return Membership.JOIN, None
|
||||||
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(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
"Guest access not allowed",
|
"User %r not in room %s, and room previews are disabled"
|
||||||
errcode=Codes.GUEST_ACCESS_FORBIDDEN,
|
% (requester.user, room_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
request.requester = requester
|
|
||||||
return requester
|
|
||||||
except KeyError:
|
|
||||||
raise MissingClientTokenError()
|
|
||||||
|
|
||||||
async def validate_appservice_can_control_user_id(
|
async def validate_appservice_can_control_user_id(
|
||||||
self, app_service: ApplicationService, user_id: str
|
self, app_service: ApplicationService, user_id: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -284,184 +172,16 @@ class Auth:
|
||||||
403, "Application service has not registered this user (%s)" % user_id
|
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:
|
async def is_server_admin(self, requester: Requester) -> bool:
|
||||||
"""Check if the given user is a local server admin.
|
"""Check if the given user is a local server admin.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
requester: The user making the request, according to the access token.
|
requester: user to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the user is an admin
|
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(
|
async def check_can_change_room_list(
|
||||||
self, room_id: str, requester: Requester
|
self, room_id: str, requester: Requester
|
||||||
|
@ -470,8 +190,8 @@ class Auth:
|
||||||
published room list.
|
published room list.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
room_id: The room to check.
|
room_id
|
||||||
requester: The user making the request, according to the access token.
|
user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
is_admin = await self.is_server_admin(requester)
|
is_admin = await self.is_server_admin(requester)
|
||||||
|
@ -518,7 +238,6 @@ class Auth:
|
||||||
return bool(query_params) or bool(auth_headers)
|
return bool(query_params) or bool(auth_headers)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@cancellable
|
|
||||||
def get_access_token_from_request(request: Request) -> str:
|
def get_access_token_from_request(request: Request) -> str:
|
||||||
"""Extracts the access_token from the request.
|
"""Extracts the access_token from the request.
|
||||||
|
|
||||||
|
@ -556,47 +275,77 @@ class Auth:
|
||||||
|
|
||||||
return query_params[0].decode("ascii")
|
return query_params[0].decode("ascii")
|
||||||
|
|
||||||
@trace
|
@cancellable
|
||||||
async def check_user_in_room_or_world_readable(
|
async def get_appservice_user(
|
||||||
self, room_id: str, requester: Requester, allow_departed_users: bool = False
|
self, request: Request, access_token: str
|
||||||
) -> Tuple[str, Optional[str]]:
|
) -> Optional[Requester]:
|
||||||
"""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.
|
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:
|
[^1] Unstable and provided by MSC3202.
|
||||||
room_id: The room to check.
|
Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
|
||||||
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.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Resolves to the current membership of the user in the room and the
|
the application service `Requester` of that request
|
||||||
membership event ID of the user. If the user is not in the room and
|
|
||||||
never has been, then `(Membership.JOIN, None)` is returned.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
Postconditions:
|
||||||
# check_user_in_room will return the most recent membership
|
- The `app_service` field in the returned `Requester` is set
|
||||||
# event for the user if:
|
- The `user_id` field in the returned `Requester` is either the application
|
||||||
# * The user is a non-guest user, and was ever in the room
|
service sender or the controlled user set by the `user_id` URI parameter
|
||||||
# * The user is a guest user, and has joined the room
|
- The returned application service is permitted to control the returned user ID.
|
||||||
# else it will throw.
|
- The returned device ID, if present, has been checked to be a valid device ID
|
||||||
return await self.check_user_in_room(
|
for the returned user ID.
|
||||||
room_id, requester, allow_departed_users=allow_departed_users
|
"""
|
||||||
)
|
DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
|
||||||
except AuthError:
|
|
||||||
visibility = await self._storage_controllers.state.get_current_state_event(
|
app_service = self.store.get_app_service_by_token(access_token)
|
||||||
room_id, EventTypes.RoomHistoryVisibility, ""
|
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 (
|
if (
|
||||||
visibility
|
self.hs.config.experimental.msc3202_device_masquerading_enabled
|
||||||
and visibility.content.get("history_visibility")
|
and DEVICE_ID_ARG_NAME in request.args
|
||||||
== HistoryVisibility.WORLD_READABLE
|
|
||||||
):
|
):
|
||||||
return Membership.JOIN, None
|
effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
|
||||||
raise UnstableSpecAuthError(
|
# We only just set this so it can't be None!
|
||||||
403,
|
assert effective_device_id is not None
|
||||||
"User %s not in room %s, and room previews are disabled"
|
device_opt = await self.store.get_device(
|
||||||
% (requester.user, room_id),
|
effective_user_id, effective_device_id
|
||||||
errcode=Codes.NOT_JOINED,
|
)
|
||||||
|
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"
|
AUTHORISING_USER: Final = "join_authorised_via_users_server"
|
||||||
|
|
||||||
# Use for mentioning users.
|
# 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
|
# an unspecced field added to to-device messages to identify them uniquely-ish
|
||||||
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
||||||
|
|
|
@ -119,14 +119,20 @@ class Codes(str, Enum):
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(RuntimeError):
|
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:
|
Attributes:
|
||||||
code: HTTP error code
|
code: HTTP error code
|
||||||
msg: string describing the error
|
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))
|
super().__init__("%d: %s" % (code, msg))
|
||||||
|
|
||||||
# Some calls to this method pass instances of http.HTTPStatus for `code`.
|
# 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.
|
# To eliminate this behaviour, we convert them to their integer equivalents here.
|
||||||
self.code = int(code)
|
self.code = int(code)
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
self.headers = headers
|
||||||
|
|
||||||
|
|
||||||
class RedirectException(CodeMessageException):
|
class RedirectException(CodeMessageException):
|
||||||
|
@ -182,6 +189,7 @@ class SynapseError(CodeMessageException):
|
||||||
msg: str,
|
msg: str,
|
||||||
errcode: str = Codes.UNKNOWN,
|
errcode: str = Codes.UNKNOWN,
|
||||||
additional_fields: Optional[Dict] = None,
|
additional_fields: Optional[Dict] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
):
|
):
|
||||||
"""Constructs a synapse error.
|
"""Constructs a synapse error.
|
||||||
|
|
||||||
|
@ -190,7 +198,7 @@ class SynapseError(CodeMessageException):
|
||||||
msg: The human-readable error message.
|
msg: The human-readable error message.
|
||||||
errcode: The matrix error code e.g 'M_FORBIDDEN'
|
errcode: The matrix error code e.g 'M_FORBIDDEN'
|
||||||
"""
|
"""
|
||||||
super().__init__(code, msg)
|
super().__init__(code, msg, headers)
|
||||||
self.errcode = errcode
|
self.errcode = errcode
|
||||||
if additional_fields is None:
|
if additional_fields is None:
|
||||||
self._additional_fields: Dict = {}
|
self._additional_fields: Dict = {}
|
||||||
|
@ -335,6 +343,20 @@ class AuthError(SynapseError):
|
||||||
super().__init__(code, msg, errcode, additional_fields)
|
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):
|
class UnstableSpecAuthError(AuthError):
|
||||||
"""An error raised when a new error code is being proposed to replace a previous one.
|
"""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,
|
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, {})
|
self.DEFAULT_FILTER_COLLECTION = FilterCollection(hs, {})
|
||||||
|
|
||||||
async def get_user_filter(
|
async def get_user_filter(
|
||||||
self, user_localpart: str, filter_id: Union[int, str]
|
self, user_id: UserID, filter_id: Union[int, str]
|
||||||
) -> "FilterCollection":
|
) -> "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)
|
return FilterCollection(self._hs, result)
|
||||||
|
|
||||||
def add_user_filter(self, user_id: UserID, user_filter: JsonDict) -> Awaitable[int]:
|
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:
|
if password_config is None:
|
||||||
password_config = {}
|
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,
|
# 'only_for_reauth' allows users who have previously set a password to use it,
|
||||||
# even though passwords would otherwise be disabled.
|
# even though passwords would otherwise be disabled.
|
||||||
passwords_for_reauth_only = passwords_enabled == "only_for_reauth"
|
passwords_for_reauth_only = passwords_enabled == "only_for_reauth"
|
||||||
|
@ -53,3 +60,13 @@ class AuthConfig(Config):
|
||||||
self.ui_auth_session_timeout = self.parse_duration(
|
self.ui_auth_session_timeout = self.parse_duration(
|
||||||
ui_auth.get("session_timeout", 0)
|
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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from typing import Any, Optional
|
import enum
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
import attr.validators
|
||||||
|
|
||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
||||||
from synapse.config import ConfigError
|
from synapse.config import ConfigError
|
||||||
from synapse.config._base import Config
|
from synapse.config._base import Config, RootConfig
|
||||||
from synapse.types import JsonDict
|
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)
|
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
||||||
class MSC3866Config:
|
class MSC3866Config:
|
||||||
|
@ -118,13 +319,6 @@ class ExperimentalConfig(Config):
|
||||||
# MSC3881: Remotely toggle push notifications for another client
|
# MSC3881: Remotely toggle push notifications for another client
|
||||||
self.msc3881_enabled: bool = experimental.get("msc3881_enabled", False)
|
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.
|
# MSC3874: Filtering /messages with rel_types / not_rel_types.
|
||||||
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)
|
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)
|
||||||
|
|
||||||
|
@ -164,11 +358,6 @@ class ExperimentalConfig(Config):
|
||||||
# MSC3391: Removing account data.
|
# MSC3391: Removing account data.
|
||||||
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
|
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.
|
# MSC3959: Do not generate notifications for edits.
|
||||||
self.msc3958_supress_edit_notifs = experimental.get(
|
self.msc3958_supress_edit_notifs = experimental.get(
|
||||||
"msc3958_supress_edit_notifs", False
|
"msc3958_supress_edit_notifs", False
|
||||||
|
@ -182,8 +371,19 @@ class ExperimentalConfig(Config):
|
||||||
"msc3981_recurse_relations", False
|
"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
|
# 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
|
# MSC4009: E.164 Matrix IDs
|
||||||
self.msc4009_e164_mxids = experimental.get("msc4009_e164_mxids", False)
|
self.msc4009_e164_mxids = experimental.get("msc4009_e164_mxids", False)
|
||||||
|
|
|
@ -22,6 +22,8 @@ class FederationConfig(Config):
|
||||||
section = "federation"
|
section = "federation"
|
||||||
|
|
||||||
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
||||||
|
federation_config = config.setdefault("federation", {})
|
||||||
|
|
||||||
# FIXME: federation_domain_whitelist needs sytests
|
# FIXME: federation_domain_whitelist needs sytests
|
||||||
self.federation_domain_whitelist: Optional[dict] = None
|
self.federation_domain_whitelist: Optional[dict] = None
|
||||||
federation_domain_whitelist = config.get("federation_domain_whitelist", 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_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"}}
|
_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 the event contains a mentions key, validate it.
|
||||||
if (
|
if EventContentFields.MENTIONS in event.content:
|
||||||
EventContentFields.MSC3952_MENTIONS in event.content
|
validate_json_object(event.content[EventContentFields.MENTIONS], Mentions)
|
||||||
and config.experimental.msc3952_intentional_mentions
|
|
||||||
):
|
|
||||||
validate_json_object(
|
|
||||||
event.content[EventContentFields.MSC3952_MENTIONS], Mentions
|
|
||||||
)
|
|
||||||
|
|
||||||
def _validate_retention(self, event: EventBase) -> None:
|
def _validate_retention(self, event: EventBase) -> None:
|
||||||
"""Checks that an event that defines the retention policy for a room respects the
|
"""Checks that an event that defines the retention policy for a room respects the
|
||||||
|
|
|
@ -515,7 +515,7 @@ class FederationServer(FederationBase):
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to handle PDU %s",
|
"Failed to handle PDU %s",
|
||||||
event_id,
|
event_id,
|
||||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore
|
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||||
)
|
)
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
@ -944,7 +944,7 @@ class FederationServer(FederationBase):
|
||||||
if not self._is_mine_server_name(authorising_server):
|
if not self._is_mine_server_name(authorising_server):
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
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(
|
event.signatures.update(
|
||||||
|
@ -1247,7 +1247,7 @@ class FederationServer(FederationBase):
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to handle PDU %s",
|
"Failed to handle PDU %s",
|
||||||
event.event_id,
|
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(
|
received_ts = await self.store.remove_received_event_from_staging(
|
||||||
|
@ -1291,9 +1291,6 @@ class FederationServer(FederationBase):
|
||||||
return
|
return
|
||||||
lock = new_lock
|
lock = new_lock
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "<ReplicationLayer(%s)>" % self.server_name
|
|
||||||
|
|
||||||
async def exchange_third_party_invite(
|
async def exchange_third_party_invite(
|
||||||
self, sender_user_id: str, target_user_id: str, room_id: str, signed: Dict
|
self, sender_user_id: str, target_user_id: str, room_id: str, signed: Dict
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -164,7 +164,7 @@ class AccountValidityHandler:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_display_name = await self.store.get_profile_displayname(
|
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:
|
if user_display_name is None:
|
||||||
user_display_name = user_id
|
user_display_name = user_id
|
||||||
|
|
|
@ -89,7 +89,7 @@ class AdminHandler:
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add additional user metadata
|
# 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())
|
threepids = await self._store.user_get_threepids(user.to_string())
|
||||||
external_ids = [
|
external_ids = [
|
||||||
({"auth_provider": auth_provider, "external_id": external_id})
|
({"auth_provider": auth_provider, "external_id": external_id})
|
||||||
|
|
|
@ -274,6 +274,8 @@ class AuthHandler:
|
||||||
# response.
|
# response.
|
||||||
self._extra_attributes: Dict[str, SsoLoginExtraAttributes] = {}
|
self._extra_attributes: Dict[str, SsoLoginExtraAttributes] = {}
|
||||||
|
|
||||||
|
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||||
|
|
||||||
async def validate_user_via_ui_auth(
|
async def validate_user_via_ui_auth(
|
||||||
self,
|
self,
|
||||||
requester: Requester,
|
requester: Requester,
|
||||||
|
@ -322,8 +324,12 @@ class AuthHandler:
|
||||||
|
|
||||||
LimitExceededError if the ratelimiter's failed request count for this
|
LimitExceededError if the ratelimiter's failed request count for this
|
||||||
user is too high to proceed
|
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:
|
if not requester.access_token_id:
|
||||||
raise ValueError("Cannot validate a user without an access token")
|
raise ValueError("Cannot validate a user without an access token")
|
||||||
if can_skip_ui_auth and self._ui_auth_session_timeout:
|
if can_skip_ui_auth and self._ui_auth_session_timeout:
|
||||||
|
@ -1753,7 +1759,7 @@ class AuthHandler:
|
||||||
return
|
return
|
||||||
|
|
||||||
user_profile_data = await self.store.get_profileinfo(
|
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.
|
# 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
|
# Add the user to the directory, if necessary. Note that
|
||||||
# this must be done after the user is re-activated, because
|
# this must be done after the user is re-activated, because
|
||||||
# deactivated users are excluded from the user directory.
|
# 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)
|
await self.user_directory_handler.handle_local_profile_change(user_id, profile)
|
||||||
|
|
|
@ -200,6 +200,7 @@ class FederationHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
@trace
|
@trace
|
||||||
|
@tag_args
|
||||||
async def maybe_backfill(
|
async def maybe_backfill(
|
||||||
self, room_id: str, current_depth: int, limit: int
|
self, room_id: str, current_depth: int, limit: int
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@ -214,6 +215,9 @@ class FederationHandler:
|
||||||
limit: The number of events that the pagination request will
|
limit: The number of events that the pagination request will
|
||||||
return. This is used as part of the heuristic to decide if we
|
return. This is used as part of the heuristic to decide if we
|
||||||
should back paginate.
|
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
|
# Starting the processing time here so we can include the room backfill
|
||||||
# linearizer lock queue in the timing
|
# linearizer lock queue in the timing
|
||||||
|
@ -227,6 +231,8 @@ class FederationHandler:
|
||||||
processing_start_time=processing_start_time,
|
processing_start_time=processing_start_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@trace
|
||||||
|
@tag_args
|
||||||
async def _maybe_backfill_inner(
|
async def _maybe_backfill_inner(
|
||||||
self,
|
self,
|
||||||
room_id: str,
|
room_id: str,
|
||||||
|
@ -247,6 +253,9 @@ class FederationHandler:
|
||||||
limit: The max number of events to request from the remote federated server.
|
limit: The max number of events to request from the remote federated server.
|
||||||
processing_start_time: The time when `maybe_backfill` started processing.
|
processing_start_time: The time when `maybe_backfill` started processing.
|
||||||
Only used for timing. If `None`, no timing observation will be made.
|
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 = [
|
backwards_extremities = [
|
||||||
_BackfillPoint(event_id, depth, _BackfillPointType.BACKWARDS_EXTREMITY)
|
_BackfillPoint(event_id, depth, _BackfillPointType.BACKWARDS_EXTREMITY)
|
||||||
|
@ -302,15 +311,30 @@ class FederationHandler:
|
||||||
len(sorted_backfill_points),
|
len(sorted_backfill_points),
|
||||||
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
|
# If we have no backfill points lower than the `current_depth` then either we
|
||||||
# either we can a) bail or b) still attempt to backfill. We opt to try
|
# can a) bail or b) still attempt to backfill. We opt to try backfilling anyway
|
||||||
# backfilling anyway just in case we do get relevant events.
|
# 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:
|
if not sorted_backfill_points and current_depth != MAX_DEPTH:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"_maybe_backfill_inner: all backfill points are *after* current depth. Trying again with later backfill points."
|
"_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,
|
room_id=room_id,
|
||||||
# We use `MAX_DEPTH` so that we find all backfill points next
|
# We use `MAX_DEPTH` so that we find all backfill points next
|
||||||
# time (all events are below the `MAX_DEPTH`)
|
# time (all events are below the `MAX_DEPTH`)
|
||||||
|
@ -321,6 +345,9 @@ class FederationHandler:
|
||||||
# overall otherwise the smaller one will throw off the results.
|
# overall otherwise the smaller one will throw off the results.
|
||||||
processing_start_time=None,
|
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
|
# Even after recursing with `MAX_DEPTH`, we didn't find any
|
||||||
# backward extremities to backfill from.
|
# backward extremities to backfill from.
|
||||||
|
|
|
@ -1354,7 +1354,7 @@ class OidcProvider:
|
||||||
finish_request(request)
|
finish_request(request)
|
||||||
|
|
||||||
|
|
||||||
class LogoutToken(JWTClaims):
|
class LogoutToken(JWTClaims): # type: ignore[misc]
|
||||||
"""
|
"""
|
||||||
Holds and verify claims of a logout token, as per
|
Holds and verify claims of a logout token, as per
|
||||||
https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken
|
https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken
|
||||||
|
|
|
@ -360,7 +360,7 @@ class PaginationHandler:
|
||||||
except Exception:
|
except Exception:
|
||||||
f = Failure()
|
f = Failure()
|
||||||
logger.error(
|
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].status = PurgeStatus.STATUS_FAILED
|
||||||
self._purges_by_id[purge_id].error = f.getErrorMessage()
|
self._purges_by_id[purge_id].error = f.getErrorMessage()
|
||||||
|
@ -689,7 +689,7 @@ class PaginationHandler:
|
||||||
f = Failure()
|
f = Failure()
|
||||||
logger.error(
|
logger.error(
|
||||||
"failed",
|
"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].status = DeleteStatus.STATUS_FAILED
|
||||||
self._delete_by_id[delete_id].error = f.getErrorMessage()
|
self._delete_by_id[delete_id].error = f.getErrorMessage()
|
||||||
|
|
|
@ -648,7 +648,6 @@ class PresenceHandler(BasePresenceHandler):
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__(hs)
|
super().__init__(hs)
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.server_name = hs.hostname
|
|
||||||
self.wheel_timer: WheelTimer[str] = WheelTimer()
|
self.wheel_timer: WheelTimer[str] = WheelTimer()
|
||||||
self.notifier = hs.get_notifier()
|
self.notifier = hs.get_notifier()
|
||||||
self._presence_enabled = hs.config.server.use_presence
|
self._presence_enabled = hs.config.server.use_presence
|
||||||
|
|
|
@ -67,7 +67,7 @@ class ProfileHandler:
|
||||||
target_user = UserID.from_string(user_id)
|
target_user = UserID.from_string(user_id)
|
||||||
|
|
||||||
if self.hs.is_mine(target_user):
|
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:
|
if profileinfo.display_name is None:
|
||||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
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]:
|
async def get_displayname(self, target_user: UserID) -> Optional[str]:
|
||||||
if self.hs.is_mine(target_user):
|
if self.hs.is_mine(target_user):
|
||||||
try:
|
try:
|
||||||
displayname = await self.store.get_profile_displayname(
|
displayname = await self.store.get_profile_displayname(target_user)
|
||||||
target_user.localpart
|
|
||||||
)
|
|
||||||
except StoreError as e:
|
except StoreError as e:
|
||||||
if e.code == 404:
|
if e.code == 404:
|
||||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
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")
|
raise AuthError(400, "Cannot set another user's displayname")
|
||||||
|
|
||||||
if not by_admin and not self.hs.config.registration.enable_set_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:
|
if profile.display_name:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
|
@ -180,7 +178,7 @@ class ProfileHandler:
|
||||||
|
|
||||||
await self.store.set_profile_displayname(target_user, displayname_to_set)
|
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(
|
await self.user_directory_handler.handle_local_profile_change(
|
||||||
target_user.to_string(), profile
|
target_user.to_string(), profile
|
||||||
)
|
)
|
||||||
|
@ -194,9 +192,7 @@ class ProfileHandler:
|
||||||
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
|
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
|
||||||
if self.hs.is_mine(target_user):
|
if self.hs.is_mine(target_user):
|
||||||
try:
|
try:
|
||||||
avatar_url = await self.store.get_profile_avatar_url(
|
avatar_url = await self.store.get_profile_avatar_url(target_user)
|
||||||
target_user.localpart
|
|
||||||
)
|
|
||||||
except StoreError as e:
|
except StoreError as e:
|
||||||
if e.code == 404:
|
if e.code == 404:
|
||||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
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")
|
raise AuthError(400, "Cannot set another user's avatar_url")
|
||||||
|
|
||||||
if not by_admin and not self.hs.config.registration.enable_set_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:
|
if profile.avatar_url:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400, "Changing avatar is disabled on this server", Codes.FORBIDDEN
|
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)
|
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(
|
await self.user_directory_handler.handle_local_profile_change(
|
||||||
target_user.to_string(), profile
|
target_user.to_string(), profile
|
||||||
)
|
)
|
||||||
|
@ -369,14 +365,10 @@ class ProfileHandler:
|
||||||
response = {}
|
response = {}
|
||||||
try:
|
try:
|
||||||
if just_field is None or just_field == "displayname":
|
if just_field is None or just_field == "displayname":
|
||||||
response["displayname"] = await self.store.get_profile_displayname(
|
response["displayname"] = await self.store.get_profile_displayname(user)
|
||||||
user.localpart
|
|
||||||
)
|
|
||||||
|
|
||||||
if just_field is None or just_field == "avatar_url":
|
if just_field is None or just_field == "avatar_url":
|
||||||
response["avatar_url"] = await self.store.get_profile_avatar_url(
|
response["avatar_url"] = await self.store.get_profile_avatar_url(user)
|
||||||
user.localpart
|
|
||||||
)
|
|
||||||
except StoreError as e:
|
except StoreError as e:
|
||||||
if e.code == 404:
|
if e.code == 404:
|
||||||
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
raise SynapseError(404, "Profile was not found", Codes.NOT_FOUND)
|
||||||
|
|
|
@ -27,7 +27,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ReadMarkerHandler:
|
class ReadMarkerHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.server_name = hs.config.server.server_name
|
|
||||||
self.store = hs.get_datastores().main
|
self.store = hs.get_datastores().main
|
||||||
self.account_data_handler = hs.get_account_data_handler()
|
self.account_data_handler = hs.get_account_data_handler()
|
||||||
self.read_marker_linearizer = Linearizer(name="read_marker")
|
self.read_marker_linearizer = Linearizer(name="read_marker")
|
||||||
|
|
|
@ -315,7 +315,7 @@ class RegistrationHandler:
|
||||||
approved=approved,
|
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(
|
await self.user_directory_handler.handle_local_profile_change(
|
||||||
user_id, profile
|
user_id, profile
|
||||||
)
|
)
|
||||||
|
|
|
@ -205,11 +205,17 @@ class RelationsHandler:
|
||||||
event_id: The event IDs to look and redact relations of.
|
event_id: The event IDs to look and redact relations of.
|
||||||
initial_redaction_event: The redaction for the event referred to by
|
initial_redaction_event: The redaction for the event referred to by
|
||||||
event_id.
|
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:
|
Raises:
|
||||||
ShadowBanError if the requester is shadow-banned
|
ShadowBanError if the requester is shadow-banned
|
||||||
"""
|
"""
|
||||||
|
if "*" in relation_types:
|
||||||
|
related_event_ids = await self._main_store.get_all_relations_for_event(
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
related_event_ids = (
|
related_event_ids = (
|
||||||
await self._main_store.get_all_relations_for_event_with_types(
|
await self._main_store.get_all_relations_for_event_with_types(
|
||||||
event_id, relation_types
|
event_id, relation_types
|
||||||
|
|
|
@ -872,6 +872,8 @@ class RoomCreationHandler:
|
||||||
visibility = config.get("visibility", "private")
|
visibility = config.get("visibility", "private")
|
||||||
is_public = visibility == "public"
|
is_public = visibility == "public"
|
||||||
|
|
||||||
|
self._validate_room_config(config, visibility)
|
||||||
|
|
||||||
room_id = await self._generate_and_create_room_id(
|
room_id = await self._generate_and_create_room_id(
|
||||||
creator_id=user_id,
|
creator_id=user_id,
|
||||||
is_public=is_public,
|
is_public=is_public,
|
||||||
|
@ -1111,20 +1113,7 @@ class RoomCreationHandler:
|
||||||
|
|
||||||
return new_event, new_unpersisted_context
|
return new_event, new_unpersisted_context
|
||||||
|
|
||||||
visibility = room_config.get("visibility", "private")
|
preset_config, config = self._room_preset_config(room_config)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# MSC2175 removes the creator field from the create event.
|
# MSC2175 removes the creator field from the create event.
|
||||||
if not room_version.msc2175_implicit_room_creator:
|
if not room_version.msc2175_implicit_room_creator:
|
||||||
|
@ -1306,6 +1295,65 @@ class RoomCreationHandler:
|
||||||
assert last_event.internal_metadata.stream_ordering is not None
|
assert last_event.internal_metadata.stream_ordering is not None
|
||||||
return last_event.internal_metadata.stream_ordering, last_event.event_id, depth
|
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:
|
def _generate_room_id(self) -> str:
|
||||||
"""Generates a random room ID.
|
"""Generates a random room ID.
|
||||||
|
|
||||||
|
@ -1490,7 +1538,6 @@ class RoomContextHandler:
|
||||||
|
|
||||||
class TimestampLookupHandler:
|
class TimestampLookupHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.server_name = hs.hostname
|
|
||||||
self.store = hs.get_datastores().main
|
self.store = hs.get_datastores().main
|
||||||
self.state_handler = hs.get_state_handler()
|
self.state_handler = hs.get_state_handler()
|
||||||
self.federation_client = hs.get_federation_client()
|
self.federation_client = hs.get_federation_client()
|
||||||
|
|
|
@ -42,7 +42,6 @@ class StatsHandler:
|
||||||
self.store = hs.get_datastores().main
|
self.store = hs.get_datastores().main
|
||||||
self._storage_controllers = hs.get_storage_controllers()
|
self._storage_controllers = hs.get_storage_controllers()
|
||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
self.server_name = hs.hostname
|
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.notifier = hs.get_notifier()
|
self.notifier = hs.get_notifier()
|
||||||
self.is_mine_id = hs.is_mine_id
|
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
|
MAXINT = sys.maxsize
|
||||||
|
|
||||||
|
|
||||||
|
@ -406,7 +404,12 @@ class MatrixFederationHttpClient:
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self._store = hs.get_datastores().main
|
self._store = hs.get_datastores().main
|
||||||
self.version_string_bytes = hs.version_string.encode("ascii")
|
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))
|
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
|
Note that the above intervals are *in addition* to the time spent
|
||||||
waiting for the request to complete (up to `timeout` ms).
|
waiting for the request to complete (up to `timeout` ms).
|
||||||
|
|
||||||
NB: the long retry algorithm takes over 20 minutes to complete, with
|
NB: the long retry algorithm takes over 20 minutes to complete, with a
|
||||||
a default timeout of 60s!
|
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
|
ignore_backoff: true to ignore the historical backoff data
|
||||||
and try the request anyway.
|
and try the request anyway.
|
||||||
|
@ -576,9 +586,9 @@ class MatrixFederationHttpClient:
|
||||||
# XXX: Would be much nicer to retry only at the transaction-layer
|
# XXX: Would be much nicer to retry only at the transaction-layer
|
||||||
# (once we have reliable transactions in place)
|
# (once we have reliable transactions in place)
|
||||||
if long_retries:
|
if long_retries:
|
||||||
retries_left = MAX_LONG_RETRIES
|
retries_left = self.max_long_retries
|
||||||
else:
|
else:
|
||||||
retries_left = MAX_SHORT_RETRIES
|
retries_left = self.max_short_retries
|
||||||
|
|
||||||
url_bytes = request.uri
|
url_bytes = request.uri
|
||||||
url_str = url_bytes.decode("ascii")
|
url_str = url_bytes.decode("ascii")
|
||||||
|
@ -723,12 +733,12 @@ class MatrixFederationHttpClient:
|
||||||
|
|
||||||
if retries_left and not timeout:
|
if retries_left and not timeout:
|
||||||
if long_retries:
|
if long_retries:
|
||||||
delay = 4 ** (MAX_LONG_RETRIES + 1 - retries_left)
|
delay = 4 ** (self.max_long_retries + 1 - retries_left)
|
||||||
delay = min(delay, 60)
|
delay = min(delay, self.max_long_retry_delay)
|
||||||
delay *= random.uniform(0.8, 1.4)
|
delay *= random.uniform(0.8, 1.4)
|
||||||
else:
|
else:
|
||||||
delay = 0.5 * 2 ** (MAX_SHORT_RETRIES - retries_left)
|
delay = 0.5 * 2 ** (self.max_short_retries - retries_left)
|
||||||
delay = min(delay, 2)
|
delay = min(delay, self.max_short_retry_delay)
|
||||||
delay *= random.uniform(0.8, 1.4)
|
delay *= random.uniform(0.8, 1.4)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
|
@ -76,7 +76,7 @@ class ReplicationEndpointFactory:
|
||||||
endpoint = wrapClientTLS(
|
endpoint = wrapClientTLS(
|
||||||
# The 'port' argument below isn't actually used by the function
|
# The 'port' argument below isn't actually used by the function
|
||||||
self.context_factory.creatorForNetloc(
|
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,
|
self.instance_map[worker_name].port,
|
||||||
),
|
),
|
||||||
endpoint,
|
endpoint,
|
||||||
|
|
|
@ -108,9 +108,12 @@ def return_json_error(
|
||||||
|
|
||||||
if f.check(SynapseError):
|
if f.check(SynapseError):
|
||||||
# mypy doesn't understand that f.check asserts the type.
|
# 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_code = exc.code
|
||||||
error_dict = exc.error_dict(config)
|
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)
|
logger.info("%s SynapseError: %s - %s", request, error_code, exc.msg)
|
||||||
elif f.check(CancelledError):
|
elif f.check(CancelledError):
|
||||||
error_code = HTTP_STATUS_REQUEST_CANCELLED
|
error_code = HTTP_STATUS_REQUEST_CANCELLED
|
||||||
|
@ -121,7 +124,7 @@ def return_json_error(
|
||||||
"Got cancellation before client disconnection from %r: %r",
|
"Got cancellation before client disconnection from %r: %r",
|
||||||
request.request_metrics.name,
|
request.request_metrics.name,
|
||||||
request,
|
request,
|
||||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
error_code = 500
|
error_code = 500
|
||||||
|
@ -131,7 +134,7 @@ def return_json_error(
|
||||||
"Failed handle request via %r: %r",
|
"Failed handle request via %r: %r",
|
||||||
request.request_metrics.name,
|
request.request_metrics.name,
|
||||||
request,
|
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,
|
# 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):
|
if f.check(CodeMessageException):
|
||||||
# mypy doesn't understand that f.check asserts the type.
|
# mypy doesn't understand that f.check asserts the type.
|
||||||
cme: CodeMessageException = f.value # type: ignore
|
cme: CodeMessageException = f.value
|
||||||
code = cme.code
|
code = cme.code
|
||||||
msg = cme.msg
|
msg = cme.msg
|
||||||
|
if cme.headers is not None:
|
||||||
|
for header, value in cme.headers.items():
|
||||||
|
request.setHeader(header, value)
|
||||||
|
|
||||||
if isinstance(cme, RedirectException):
|
if isinstance(cme, RedirectException):
|
||||||
logger.info("%s redirect to %s", request, cme.location)
|
logger.info("%s redirect to %s", request, cme.location)
|
||||||
|
@ -183,7 +189,7 @@ def return_html_error(
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed handle request %r",
|
"Failed handle request %r",
|
||||||
request,
|
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):
|
elif f.check(CancelledError):
|
||||||
code = HTTP_STATUS_REQUEST_CANCELLED
|
code = HTTP_STATUS_REQUEST_CANCELLED
|
||||||
|
@ -193,7 +199,7 @@ def return_html_error(
|
||||||
logger.error(
|
logger.error(
|
||||||
"Got cancellation before client disconnection when handling request %r",
|
"Got cancellation before client disconnection when handling request %r",
|
||||||
request,
|
request,
|
||||||
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore[arg-type]
|
exc_info=(f.type, f.value, f.getTracebackObject()),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
@ -202,7 +208,7 @@ def return_html_error(
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed handle request %r",
|
"Failed handle request %r",
|
||||||
request,
|
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):
|
if isinstance(error_template, str):
|
||||||
|
|
|
@ -171,6 +171,7 @@ from functools import wraps
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
Collection,
|
Collection,
|
||||||
ContextManager,
|
ContextManager,
|
||||||
|
@ -903,6 +904,7 @@ def _custom_sync_async_decorator(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if inspect.iscoroutinefunction(func):
|
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
|
# In this branch, R = Awaitable[RInner], for some other type RInner
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def _wrapper(
|
async def _wrapper(
|
||||||
|
@ -914,15 +916,16 @@ def _custom_sync_async_decorator(
|
||||||
return await func(*args, **kwargs) # type: ignore[misc]
|
return await func(*args, **kwargs) # type: ignore[misc]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# The other case here handles both sync functions and those
|
# The other case here handles sync functions including those decorated with
|
||||||
# decorated with inlineDeferred.
|
# `@defer.inlineCallbacks` or that return a `Deferred` or other `Awaitable`.
|
||||||
@wraps(func)
|
@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 = wrapping_logic(func, *args, **kwargs)
|
||||||
scope.__enter__()
|
scope.__enter__()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
|
|
||||||
if isinstance(result, defer.Deferred):
|
if isinstance(result, defer.Deferred):
|
||||||
|
|
||||||
def call_back(result: R) -> R:
|
def call_back(result: R) -> R:
|
||||||
|
@ -930,20 +933,32 @@ def _custom_sync_async_decorator(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def err_back(result: R) -> R:
|
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)
|
scope.__exit__(None, None, None)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result.addCallbacks(call_back, err_back)
|
result.addCallbacks(call_back, err_back)
|
||||||
|
|
||||||
else:
|
elif inspect.isawaitable(result):
|
||||||
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__,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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)
|
scope.__exit__(None, None, None)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
import html
|
import html
|
||||||
import logging
|
import logging
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import TYPE_CHECKING, List, Optional
|
from typing import TYPE_CHECKING, List, Optional, cast
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ class OEmbedProvider:
|
||||||
# No match.
|
# No match.
|
||||||
return None
|
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.
|
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.
|
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.
|
# Search for link elements with the proper rel and type attributes.
|
||||||
for tag in tree.xpath(
|
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||||
"//link[@rel='alternate'][@type='application/json+oembed']"
|
for tag in cast(
|
||||||
|
List["etree._Element"],
|
||||||
|
tree.xpath("//link[@rel='alternate'][@type='application/json+oembed']"),
|
||||||
):
|
):
|
||||||
if "href" in tag.attrib:
|
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.
|
# Some providers (e.g. Flickr) use alternative instead of alternate.
|
||||||
for tag in tree.xpath(
|
# Cast: the type returned by xpath depends on the xpath expression: mypy can't deduce this.
|
||||||
"//link[@rel='alternative'][@type='application/json+oembed']"
|
for tag in cast(
|
||||||
|
List["etree._Element"],
|
||||||
|
tree.xpath("//link[@rel='alternative'][@type='application/json+oembed']"),
|
||||||
):
|
):
|
||||||
if "href" in tag.attrib:
|
if "href" in tag.attrib:
|
||||||
return tag.attrib["href"]
|
return cast(str, tag.attrib["href"])
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -212,11 +216,12 @@ class OEmbedProvider:
|
||||||
return OEmbedResult(open_graph_response, author_name, cache_age)
|
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 = []
|
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:
|
if "src" in tag.attrib:
|
||||||
results.append(tag.attrib["src"])
|
results.append(cast(str, tag.attrib["src"]))
|
||||||
return results
|
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")
|
parser = etree.HTMLParser(recover=True, encoding="utf-8")
|
||||||
|
|
||||||
# Attempt to parse the body. If this fails, log and return no metadata.
|
# 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.
|
# The data was successfully parsed, but no tree was found.
|
||||||
if tree is None:
|
if tree is None:
|
||||||
return
|
return # type: ignore[unreachable]
|
||||||
|
|
||||||
# Attempt to find interesting URLs (images, videos, embeds).
|
# Attempt to find interesting URLs (images, videos, embeds).
|
||||||
if "og:image" not in open_graph_response:
|
if "og:image" not in open_graph_response:
|
||||||
|
|
|
@ -24,6 +24,7 @@ from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
Union,
|
Union,
|
||||||
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -115,7 +116,7 @@ def _get_html_media_encodings(
|
||||||
|
|
||||||
def decode_body(
|
def decode_body(
|
||||||
body: bytes, uri: str, content_type: Optional[str] = None
|
body: bytes, uri: str, content_type: Optional[str] = None
|
||||||
) -> Optional["etree.Element"]:
|
) -> Optional["etree._Element"]:
|
||||||
"""
|
"""
|
||||||
This uses lxml to parse the HTML document.
|
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
|
# Attempt to parse the body. Returns None if the body was successfully
|
||||||
# parsed, but no tree was found.
|
# 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(
|
def _get_meta_tags(
|
||||||
tree: "etree.Element",
|
tree: "etree._Element",
|
||||||
property: str,
|
property: str,
|
||||||
prefix: str,
|
prefix: str,
|
||||||
property_mapper: Optional[Callable[[str], Optional[str]]] = None,
|
property_mapper: Optional[Callable[[str], Optional[str]]] = None,
|
||||||
|
@ -175,9 +177,15 @@ def _get_meta_tags(
|
||||||
Returns:
|
Returns:
|
||||||
A map of tag name to value.
|
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]] = {}
|
results: Dict[str, Optional[str]] = {}
|
||||||
for tag in tree.xpath(
|
# 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='')]"
|
f"//*/meta[starts-with(@{property}, '{prefix}:')][@content][not(@content='')]"
|
||||||
|
),
|
||||||
):
|
):
|
||||||
# if we've got more than 50 tags, someone is taking the piss
|
# if we've got more than 50 tags, someone is taking the piss
|
||||||
if len(results) >= 50:
|
if len(results) >= 50:
|
||||||
|
@ -187,14 +195,15 @@ def _get_meta_tags(
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
key = tag.attrib[property]
|
key = cast(str, tag.attrib[property])
|
||||||
if property_mapper:
|
if property_mapper:
|
||||||
key = property_mapper(key)
|
new_key = property_mapper(key)
|
||||||
# None is a special value used to ignore a value.
|
# None is a special value used to ignore a value.
|
||||||
if key is None:
|
if new_key is None:
|
||||||
continue
|
continue
|
||||||
|
key = new_key
|
||||||
|
|
||||||
results[key] = tag.attrib["content"]
|
results[key] = cast(str, tag.attrib["content"])
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@ -219,7 +228,7 @@ def _map_twitter_to_open_graph(key: str) -> Optional[str]:
|
||||||
return "og" + key[7:]
|
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.
|
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:
|
if "og:title" not in og:
|
||||||
# Attempt to find a title from the title tag, or the biggest header on the page.
|
# 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:
|
if title:
|
||||||
og["og:title"] = title[0].strip()
|
og["og:title"] = title[0].strip()
|
||||||
else:
|
else:
|
||||||
og["og:title"] = None
|
og["og:title"] = None
|
||||||
|
|
||||||
if "og:image" not in og:
|
if "og:image" not in og:
|
||||||
meta_image = tree.xpath(
|
# 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]"
|
"//*/meta[translate(@itemprop, 'IMAGE', 'image')='image'][not(@content='')]/@content[1]"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
# If a meta image is found, use it.
|
# If a meta image is found, use it.
|
||||||
if meta_image:
|
if meta_image:
|
||||||
og["og:image"] = meta_image[0]
|
og["og:image"] = meta_image[0]
|
||||||
else:
|
else:
|
||||||
# Try to find images which are larger than 10px by 10px.
|
# 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
|
# 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 = sorted(
|
||||||
images,
|
images,
|
||||||
key=lambda i: (
|
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 no images were found, try to find *any* images.
|
||||||
if not 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:
|
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.
|
# Finally, fallback to the favicon if nothing else.
|
||||||
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:
|
if favicons:
|
||||||
og["og:image"] = favicons[0]
|
og["og:image"] = favicons[0]
|
||||||
|
|
||||||
if "og:description" not in og:
|
if "og:description" not in og:
|
||||||
# Check the first meta description tag for content.
|
# Check the first meta description tag for content.
|
||||||
meta_description = tree.xpath(
|
# 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]"
|
"//*/meta[translate(@name, 'DESCRIPTION', 'description')='description'][not(@content='')]/@content[1]"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
# If a meta description is found with content, use it.
|
# If a meta description is found with content, use it.
|
||||||
if meta_description:
|
if meta_description:
|
||||||
|
@ -332,7 +362,7 @@ def parse_html_to_open_graph(tree: "etree.Element") -> Dict[str, Optional[str]]:
|
||||||
return og
|
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.
|
Calculate a text description based on an HTML document.
|
||||||
|
|
||||||
|
@ -368,6 +398,9 @@ def parse_html_description(tree: "etree.Element") -> Optional[str]:
|
||||||
"canvas",
|
"canvas",
|
||||||
"img",
|
"img",
|
||||||
"picture",
|
"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,
|
etree.Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,8 +414,8 @@ def parse_html_description(tree: "etree.Element") -> Optional[str]:
|
||||||
|
|
||||||
|
|
||||||
def _iterate_over_text(
|
def _iterate_over_text(
|
||||||
tree: Optional["etree.Element"],
|
tree: Optional["etree._Element"],
|
||||||
tags_to_ignore: Set[Union[str, "etree.Comment"]],
|
tags_to_ignore: Set[object],
|
||||||
stack_limit: int = 1024,
|
stack_limit: int = 1024,
|
||||||
) -> Generator[str, None, None]:
|
) -> Generator[str, None, None]:
|
||||||
"""Iterate over the tree returning text nodes in a depth first fashion,
|
"""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
|
# This is a stack whose items are elements to iterate over *or* strings
|
||||||
# to be returned.
|
# to be returned.
|
||||||
elements: List[Union[str, "etree.Element"]] = [tree]
|
elements: List[Union[str, "etree._Element"]] = [tree]
|
||||||
while elements:
|
while elements:
|
||||||
el = elements.pop()
|
el = elements.pop()
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,8 @@ RegistryProxy = cast(CollectorRegistry, _RegistryProxy)
|
||||||
|
|
||||||
@attr.s(slots=True, hash=True, auto_attribs=True)
|
@attr.s(slots=True, hash=True, auto_attribs=True)
|
||||||
class LaterGauge(Collector):
|
class LaterGauge(Collector):
|
||||||
|
"""A Gauge which periodically calls a user-provided callback to produce metrics."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
desc: str
|
desc: str
|
||||||
labels: Optional[Sequence[str]] = attr.ib(hash=False)
|
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 import errors
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.config import ConfigError
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.presence_router import (
|
from synapse.events.presence_router import (
|
||||||
GET_INTERESTED_USERS_CALLBACK,
|
GET_INTERESTED_USERS_CALLBACK,
|
||||||
|
@ -121,6 +122,7 @@ from synapse.types import (
|
||||||
JsonMapping,
|
JsonMapping,
|
||||||
Requester,
|
Requester,
|
||||||
RoomAlias,
|
RoomAlias,
|
||||||
|
RoomID,
|
||||||
StateMap,
|
StateMap,
|
||||||
UserID,
|
UserID,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
@ -252,6 +254,7 @@ class ModuleApi:
|
||||||
self._device_handler = hs.get_device_handler()
|
self._device_handler = hs.get_device_handler()
|
||||||
self.custom_template_dir = hs.config.server.custom_template_directory
|
self.custom_template_dir = hs.config.server.custom_template_directory
|
||||||
self._callbacks = hs.get_module_api_callbacks()
|
self._callbacks = hs.get_module_api_callbacks()
|
||||||
|
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_name = self._hs.config.email.email_app_name
|
app_name = self._hs.config.email.email_app_name
|
||||||
|
@ -419,6 +422,11 @@ class ModuleApi:
|
||||||
|
|
||||||
Added in Synapse v1.46.0.
|
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(
|
return self._password_auth_provider.register_password_auth_provider_callbacks(
|
||||||
check_3pid_auth=check_3pid_auth,
|
check_3pid_auth=check_3pid_auth,
|
||||||
on_logged_out=on_logged_out,
|
on_logged_out=on_logged_out,
|
||||||
|
@ -647,7 +655,9 @@ class ModuleApi:
|
||||||
Returns:
|
Returns:
|
||||||
The profile information (i.e. display name and avatar URL).
|
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]]:
|
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
|
"""Look up the threepids (email addresses and phone numbers) associated with the
|
||||||
|
@ -1563,6 +1573,32 @@ class ModuleApi:
|
||||||
start_timestamp, end_timestamp
|
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]]:
|
async def lookup_room_alias(self, room_alias: str) -> Tuple[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
Get the room ID associated with a room alias.
|
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.should_calculate_push_rules = self.hs.config.push.enable_push
|
||||||
|
|
||||||
self._related_event_match_enabled = self.hs.config.experimental.msc3664_enabled
|
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(
|
self.room_push_rule_cache_metrics = register_cache(
|
||||||
"cache",
|
"cache",
|
||||||
|
@ -390,10 +387,7 @@ class BulkPushRuleEvaluator:
|
||||||
del notification_levels[key]
|
del notification_levels[key]
|
||||||
|
|
||||||
# Pull out any user and room mentions.
|
# Pull out any user and room mentions.
|
||||||
has_mentions = (
|
has_mentions = EventContentFields.MENTIONS in event.content
|
||||||
self._intentional_mentions_enabled
|
|
||||||
and EventContentFields.MSC3952_MENTIONS in event.content
|
|
||||||
)
|
|
||||||
|
|
||||||
evaluator = PushRuleEvaluator(
|
evaluator = PushRuleEvaluator(
|
||||||
_flatten_dict(event),
|
_flatten_dict(event),
|
||||||
|
|
|
@ -247,7 +247,7 @@ class Mailer:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_display_name = await self.store.get_profile_displayname(
|
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:
|
if user_display_name is None:
|
||||||
user_display_name = user_id
|
user_display_name = user_id
|
||||||
|
|
|
@ -257,8 +257,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
DeleteRoomStatusByRoomIdRestServlet(hs).register(http_server)
|
DeleteRoomStatusByRoomIdRestServlet(hs).register(http_server)
|
||||||
JoinRoomAliasServlet(hs).register(http_server)
|
JoinRoomAliasServlet(hs).register(http_server)
|
||||||
VersionServlet(hs).register(http_server)
|
VersionServlet(hs).register(http_server)
|
||||||
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
UserAdminServlet(hs).register(http_server)
|
UserAdminServlet(hs).register(http_server)
|
||||||
UserMembershipRestServlet(hs).register(http_server)
|
UserMembershipRestServlet(hs).register(http_server)
|
||||||
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
UserTokenRestServlet(hs).register(http_server)
|
UserTokenRestServlet(hs).register(http_server)
|
||||||
UserRestServletV2(hs).register(http_server)
|
UserRestServletV2(hs).register(http_server)
|
||||||
UsersRestServletV2(hs).register(http_server)
|
UsersRestServletV2(hs).register(http_server)
|
||||||
|
@ -274,6 +276,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
RoomEventContextServlet(hs).register(http_server)
|
RoomEventContextServlet(hs).register(http_server)
|
||||||
RateLimitRestServlet(hs).register(http_server)
|
RateLimitRestServlet(hs).register(http_server)
|
||||||
UsernameAvailableRestServlet(hs).register(http_server)
|
UsernameAvailableRestServlet(hs).register(http_server)
|
||||||
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
ListRegistrationTokensRestServlet(hs).register(http_server)
|
ListRegistrationTokensRestServlet(hs).register(http_server)
|
||||||
NewRegistrationTokenRestServlet(hs).register(http_server)
|
NewRegistrationTokenRestServlet(hs).register(http_server)
|
||||||
RegistrationTokenRestServlet(hs).register(http_server)
|
RegistrationTokenRestServlet(hs).register(http_server)
|
||||||
|
@ -306,8 +309,10 @@ def register_servlets_for_client_rest_resource(
|
||||||
# The following resources can only be run on the main process.
|
# The following resources can only be run on the main process.
|
||||||
if hs.config.worker.worker_app is None:
|
if hs.config.worker.worker_app is None:
|
||||||
DeactivateAccountRestServlet(hs).register(http_server)
|
DeactivateAccountRestServlet(hs).register(http_server)
|
||||||
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
ResetPasswordRestServlet(hs).register(http_server)
|
ResetPasswordRestServlet(hs).register(http_server)
|
||||||
SearchUsersRestServlet(hs).register(http_server)
|
SearchUsersRestServlet(hs).register(http_server)
|
||||||
|
if not hs.config.experimental.msc3861.enabled:
|
||||||
UserRegisterServlet(hs).register(http_server)
|
UserRegisterServlet(hs).register(http_server)
|
||||||
AccountValidityRenewServlet(hs).register(http_server)
|
AccountValidityRenewServlet(hs).register(http_server)
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ class UsersRestServletV2(RestServlet):
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.admin_handler = hs.get_admin_handler()
|
self.admin_handler = hs.get_admin_handler()
|
||||||
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
|
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]:
|
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||||
await assert_requester_is_admin(self.auth, request)
|
await assert_requester_is_admin(self.auth, request)
|
||||||
|
@ -94,7 +95,14 @@ class UsersRestServletV2(RestServlet):
|
||||||
|
|
||||||
user_id = parse_string(request, "user_id")
|
user_id = parse_string(request, "user_id")
|
||||||
name = parse_string(request, "name")
|
name = parse_string(request, "name")
|
||||||
|
|
||||||
guests = parse_boolean(request, "guests", default=True)
|
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)
|
deactivated = parse_boolean(request, "deactivated", default=False)
|
||||||
|
|
||||||
# If support for MSC3866 is not enabled, apply no filtering based on the
|
# 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