mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
86 Commits
v1.129.0
...
anoa/allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2fe05704b | ||
|
|
fae72f181b | ||
|
|
2436512a25 | ||
|
|
d82ad6e554 | ||
|
|
58e8521313 | ||
|
|
3680de63a7 | ||
|
|
c8733be8aa | ||
|
|
07468a0f1c | ||
|
|
33ba8860c4 | ||
|
|
24e849e483 | ||
|
|
1624073191 | ||
|
|
ed6b7ba9c3 | ||
|
|
b7d4841947 | ||
|
|
553e124f76 | ||
|
|
99cbd33630 | ||
|
|
4b1d9d5d0e | ||
|
|
f92c6455ef | ||
|
|
a36f3a6d87 | ||
|
|
9d43bec326 | ||
|
|
a6cb3533db | ||
|
|
303c5c4daa | ||
|
|
1f4ae2f9eb | ||
|
|
67920c0aca | ||
|
|
17e6b32966 | ||
|
|
afeb0e01c5 | ||
|
|
cd1a3ac584 | ||
|
|
b3b24c69fc | ||
|
|
fa4a00a2da | ||
|
|
7d4c3b64e3 | ||
|
|
078cefd014 | ||
|
|
74e2f028bb | ||
|
|
0afdc0fc7f | ||
|
|
f5ed52c1e2 | ||
|
|
44ae5362fd | ||
|
|
194b923a6e | ||
|
|
a3bbd7eeab | ||
|
|
6e910e2b2c | ||
|
|
2db54c88ff | ||
|
|
480d4faa38 | ||
|
|
ba2f1be891 | ||
|
|
c626d54cea | ||
|
|
99c15f4630 | ||
|
|
09b4109c2e | ||
|
|
40ce11ded0 | ||
|
|
3dade08e7c | ||
|
|
1920dfff40 | ||
|
|
b7728a2df1 | ||
|
|
c6dfe70014 | ||
|
|
b5d94f654c | ||
|
|
7c633f1a58 | ||
|
|
ae877aa101 | ||
|
|
740fc885cd | ||
|
|
d0873d549a | ||
|
|
c9adbc6a1c | ||
|
|
9f9eb56333 | ||
|
|
fe8bb620de | ||
|
|
b8146d4b03 | ||
|
|
411d239db4 | ||
|
|
d18edf67d6 | ||
|
|
fd5d3d852d | ||
|
|
ea376126a0 | ||
|
|
74be5cfdbc | ||
|
|
f2ca2e31f7 | ||
|
|
6dc1ecd359 | ||
|
|
2965c9970c | ||
|
|
d59bbd8b6b | ||
|
|
7be6c711d4 | ||
|
|
5ab05e7b95 | ||
|
|
7563b2a2a3 | ||
|
|
4097ada89f | ||
|
|
f79811ed80 | ||
|
|
4eaab31757 | ||
|
|
ad140130cc | ||
|
|
e47de2b32d | ||
|
|
0384fd72ee | ||
|
|
75832f25b0 | ||
|
|
7346760aed | ||
|
|
b0795d0cb6 | ||
|
|
2ef7824620 | ||
|
|
39e17856a3 | ||
|
|
4c958c679a | ||
|
|
a87981f673 | ||
|
|
2ff977a6c3 | ||
|
|
1482ad1917 | ||
|
|
5b89c92643 | ||
|
|
33824495ba |
@@ -120,6 +120,7 @@ sytest_tests = [
|
||||
"postgres": "multi-postgres",
|
||||
"workers": "workers",
|
||||
"reactor": "asyncio",
|
||||
"failure_allowed": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,5 +9,4 @@
|
||||
- End with either a period (.) or an exclamation mark (!).
|
||||
- Start with a capital letter.
|
||||
- Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
|
||||
* [ ] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct
|
||||
(run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
|
||||
* [ ] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: docker buildx inspect
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
- name: Build and push all platforms
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
with:
|
||||
push: true
|
||||
labels: |
|
||||
|
||||
2
.github/workflows/docs-pr.yaml
vendored
2
.github/workflows/docs-pr.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
mdbook-version: '0.4.17'
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
|
||||
14
.github/workflows/docs.yaml
vendored
14
.github/workflows/docs.yaml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
run: echo 'window.SYNAPSE_VERSION = "${{ needs.pre.outputs.branch-version }}";' > ./docs/website_files/version.js
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
@@ -78,6 +78,18 @@ jobs:
|
||||
mdbook build
|
||||
cp book/welcome_and_overview.html book/index.html
|
||||
|
||||
- name: Prepare and publish schema files
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y yq
|
||||
mkdir -p book/schema
|
||||
# Remove developer notice before publishing.
|
||||
rm schema/v*/Do\ not\ edit\ files\ in\ this\ folder
|
||||
# Copy schema files that are independent from current Synapse version.
|
||||
cp -r -t book/schema schema/v*/
|
||||
# Convert config schema from YAML source file to JSON.
|
||||
yq < schema/synapse-config.schema.yaml \
|
||||
> book/schema/synapse-config.schema.json
|
||||
|
||||
# Deploy to the target directory.
|
||||
- name: Deploy to gh pages
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||
|
||||
2
.github/workflows/fix_lint.yaml
vendored
2
.github/workflows/fix_lint.yaml
vendored
@@ -44,6 +44,6 @@ jobs:
|
||||
- run: cargo fmt
|
||||
continue-on-error: true
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@e348103e9026cc0eee72ae06630dbe30c8bf7a79 # v5.1.0
|
||||
- uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
|
||||
with:
|
||||
commit_message: "Attempt to fix linting"
|
||||
|
||||
4
.github/workflows/latest_deps.yml
vendored
4
.github/workflows/latest_deps.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_INITDB_ARGS="--lc-collate C --lc-ctype C --encoding UTF8" \
|
||||
postgres:${{ matrix.postgres-version }}
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- run: pip install .[all,test]
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
- name: Prepare Complement's Prerequisites
|
||||
run: synapse/.ci/scripts/setup_complement_prerequisites.sh
|
||||
|
||||
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
cache-dependency-path: complement/go.sum
|
||||
go-version-file: complement/go.mod
|
||||
|
||||
2
.github/workflows/poetry_lockfile.yaml
vendored
2
.github/workflows/poetry_lockfile.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- run: pip install tomli
|
||||
|
||||
12
.github/workflows/release-artifacts.yml
vendored
12
.github/workflows/release-artifacts.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- id: set-distros
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
# setup-python@v4 doesn't impose a default python version. Need to use 3.x
|
||||
# here, because `python` on osx points to Python 2.7.
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all workflow run artifacts
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- name: Build a tarball for the debs
|
||||
# We need to merge all the debs uploads into one folder, then compress
|
||||
# that.
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
tar -cvJf debs.tar.xz debs
|
||||
- name: Attach to release
|
||||
# Pinned to work around https://github.com/softprops/action-gh-release/issues/445
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v0.1.15
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
54
.github/workflows/schema.yaml
vendored
Normal file
54
.github/workflows/schema.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Schema
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- schema/**
|
||||
- docs/usage/configuration/config_documentation.md
|
||||
|
||||
jobs:
|
||||
validate-schema:
|
||||
name: Ensure Synapse config schema is valid
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install check-jsonschema
|
||||
run: pip install check-jsonschema==0.33.0
|
||||
|
||||
- name: Validate meta schema
|
||||
run: check-jsonschema --check-metaschema schema/v*/meta.schema.json
|
||||
- name: Validate schema
|
||||
run: |-
|
||||
# Please bump on introduction of a new meta schema.
|
||||
LATEST_META_SCHEMA_VERSION=v1
|
||||
check-jsonschema \
|
||||
--schemafile="schema/$LATEST_META_SCHEMA_VERSION/meta.schema.json" \
|
||||
schema/synapse-config.schema.yaml
|
||||
- name: Validate default config
|
||||
# Populates the empty instance with default values and checks against the schema.
|
||||
run: |-
|
||||
echo "{}" | check-jsonschema \
|
||||
--fill-defaults --schemafile=schema/synapse-config.schema.yaml -
|
||||
|
||||
check-doc-generation:
|
||||
name: Ensure generated documentation is up-to-date
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install PyYAML
|
||||
run: pip install PyYAML==6.0.2
|
||||
|
||||
- name: Regenerate config documentation
|
||||
run: |
|
||||
scripts-dev/gen_config_documentation.py \
|
||||
schema/synapse-config.schema.yaml \
|
||||
> docs/usage/configuration/config_documentation.md
|
||||
- name: Error in case of any differences
|
||||
# Errors if there are now any modified files (untracked files are ignored).
|
||||
run: 'git diff || ! git status --porcelain=1 | grep "^ M"'
|
||||
19
.github/workflows/tests.yml
vendored
19
.github/workflows/tests.yml
vendored
@@ -102,7 +102,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- run: "pip install 'click==8.1.1' 'GitPython>=3.1.20'"
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- run: .ci/scripts/check_lockfile.py
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- run: "pip install 'towncrier>=18.6.0rc1'"
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.linting_readme == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- run: "pip install rstcheck"
|
||||
@@ -327,7 +327,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- id: get-matrix
|
||||
@@ -414,7 +414,7 @@ jobs:
|
||||
sudo apt-get -qq install build-essential libffi-dev python3-dev \
|
||||
libxml2-dev libxslt-dev xmlsec1 zlib1g-dev libjpeg-dev libwebp-dev
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
@@ -525,6 +525,11 @@ jobs:
|
||||
- name: Run SyTest
|
||||
run: /bootstrap.sh synapse
|
||||
working-directory: /src
|
||||
# Prevent failures of this configuration from causing all of CI to
|
||||
# marked as failed. This is useful for testing a new Synapse configuration
|
||||
# in anger without causing sporatic CI failures.
|
||||
continue-on-error: ${{ matrix.job.failure_allowed || false }}
|
||||
|
||||
- name: Summarise results.tap
|
||||
if: ${{ always() }}
|
||||
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
||||
@@ -669,7 +674,7 @@ jobs:
|
||||
- name: Prepare Complement's Prerequisites
|
||||
run: synapse/.ci/scripts/setup_complement_prerequisites.sh
|
||||
|
||||
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
cache-dependency-path: complement/go.sum
|
||||
go-version-file: complement/go.mod
|
||||
|
||||
2
.github/workflows/triage_labelled.yml
vendored
2
.github/workflows/triage_labelled.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
if: >
|
||||
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
||||
steps:
|
||||
- uses: actions/add-to-project@280af8ae1f83a494cfad2cb10f02f6d13529caa9 # main (v1.0.2 + 10 commits)
|
||||
- uses: actions/add-to-project@5b1a254a3546aef88e0a7724a77a623fa2e47c36 # main (v1.0.2 + 10 commits)
|
||||
id: add_project
|
||||
with:
|
||||
project-url: "https://github.com/orgs/matrix-org/projects/67"
|
||||
|
||||
2
.github/workflows/twisted_trunk.yml
vendored
2
.github/workflows/twisted_trunk.yml
vendored
@@ -173,7 +173,7 @@ jobs:
|
||||
- name: Prepare Complement's Prerequisites
|
||||
run: synapse/.ci/scripts/setup_complement_prerequisites.sh
|
||||
|
||||
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
|
||||
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
cache-dependency-path: complement/go.sum
|
||||
go-version-file: complement/go.mod
|
||||
|
||||
73
CHANGES.md
73
CHANGES.md
@@ -1,3 +1,76 @@
|
||||
# Synapse 1.130.0 (2025-05-20)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fix startup being blocked on creating a new index that was introduced in v1.130.0rc1. ([\#18439](https://github.com/element-hq/synapse/issues/18439))
|
||||
- Fix the ordering of local messages in rooms that were affected by [GHSA-v56r-hwv5-mxg6](https://github.com/advisories/GHSA-v56r-hwv5-mxg6). ([\#18447](https://github.com/element-hq/synapse/issues/18447))
|
||||
|
||||
|
||||
|
||||
|
||||
# Synapse 1.130.0rc1 (2025-05-13)
|
||||
|
||||
### Features
|
||||
|
||||
- Add an Admin API endpoint `GET /_synapse/admin/v1/scheduled_tasks` to fetch scheduled tasks. ([\#18214](https://github.com/element-hq/synapse/issues/18214))
|
||||
- Add config option `user_directory.exclude_remote_users` which, when enabled, excludes remote users from user directory search results. ([\#18300](https://github.com/element-hq/synapse/issues/18300))
|
||||
- Add support for handling `GET /devices/` on workers. ([\#18355](https://github.com/element-hq/synapse/issues/18355))
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fix a longstanding bug where Synapse would immediately retry a failing push endpoint when a new event is received, ignoring any backoff timers. ([\#18363](https://github.com/element-hq/synapse/issues/18363))
|
||||
- Pass leave from remote invite rejection down Sliding Sync. ([\#18375](https://github.com/element-hq/synapse/issues/18375))
|
||||
|
||||
### Updates to the Docker image
|
||||
|
||||
- In `configure_workers_and_start.py`, use the same absolute path of Python in the interpreter shebang, and invoke child Python processes with `sys.executable`. ([\#18291](https://github.com/element-hq/synapse/issues/18291))
|
||||
- Optimize the build of the workers image. ([\#18292](https://github.com/element-hq/synapse/issues/18292))
|
||||
- In `start_for_complement.sh`, replace some external program calls with shell builtins. ([\#18293](https://github.com/element-hq/synapse/issues/18293))
|
||||
- When generating container scripts from templates, don't add a leading newline so that their shebangs may be handled correctly. ([\#18295](https://github.com/element-hq/synapse/issues/18295))
|
||||
|
||||
### Improved Documentation
|
||||
|
||||
- Improve formatting of the README file. ([\#18218](https://github.com/element-hq/synapse/issues/18218))
|
||||
- Add documentation for configuring [Pocket ID](https://github.com/pocket-id/pocket-id) as an OIDC provider. ([\#18237](https://github.com/element-hq/synapse/issues/18237))
|
||||
- Fix typo in docs about the `push` config option. Contributed by @HarHarLinks. ([\#18320](https://github.com/element-hq/synapse/issues/18320))
|
||||
- Add `/_matrix/federation/v1/version` to list of federation endpoints that can be handled by workers. ([\#18377](https://github.com/element-hq/synapse/issues/18377))
|
||||
- Add an Admin API endpoint `GET /_synapse/admin/v1/scheduled_tasks` to fetch scheduled tasks. ([\#18384](https://github.com/element-hq/synapse/issues/18384))
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Return specific error code when adding an email address / phone number to account is not supported ([MSC4178](https://github.com/matrix-org/matrix-spec-proposals/pull/4178)). ([\#17578](https://github.com/element-hq/synapse/issues/17578))
|
||||
- Stop auto-provisionning missing users & devices when delegating auth to Matrix Authentication Service. Requires MAS 0.13.0 or later. ([\#18181](https://github.com/element-hq/synapse/issues/18181))
|
||||
- Apply file hashing and existing quarantines to media downloaded for URL previews. ([\#18297](https://github.com/element-hq/synapse/issues/18297))
|
||||
- Allow a few admin APIs used by matrix-authentication-service to run on workers. ([\#18313](https://github.com/element-hq/synapse/issues/18313))
|
||||
- Apply `should_drop_federated_event` to federation invites. ([\#18330](https://github.com/element-hq/synapse/issues/18330))
|
||||
- Allow `/rooms/` admin API to be run on workers. ([\#18360](https://github.com/element-hq/synapse/issues/18360))
|
||||
- Minor performance improvements to the notifier. ([\#18367](https://github.com/element-hq/synapse/issues/18367))
|
||||
- Slight performance increase when using the ratelimiter. ([\#18369](https://github.com/element-hq/synapse/issues/18369))
|
||||
- Don't validate the `at_hash` (access token hash) field in OIDC ID Tokens if we don't end up actually using the OIDC Access Token. ([\#18374](https://github.com/element-hq/synapse/issues/18374), [\#18385](https://github.com/element-hq/synapse/issues/18385))
|
||||
- Fixed test failures when using authlib 1.5.2. ([\#18390](https://github.com/element-hq/synapse/issues/18390))
|
||||
- Refactor [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Simplified Sliding Sync room list tests to cover both new and fallback logic paths. ([\#18399](https://github.com/element-hq/synapse/issues/18399))
|
||||
|
||||
|
||||
|
||||
### Updates to locked dependencies
|
||||
|
||||
* Bump actions/add-to-project from 280af8ae1f83a494cfad2cb10f02f6d13529caa9 to 5b1a254a3546aef88e0a7724a77a623fa2e47c36. ([\#18365](https://github.com/element-hq/synapse/issues/18365))
|
||||
* Bump actions/download-artifact from 4.2.1 to 4.3.0. ([\#18364](https://github.com/element-hq/synapse/issues/18364))
|
||||
* Bump actions/setup-go from 5.4.0 to 5.5.0. ([\#18426](https://github.com/element-hq/synapse/issues/18426))
|
||||
* Bump anyhow from 1.0.97 to 1.0.98. ([\#18336](https://github.com/element-hq/synapse/issues/18336))
|
||||
* Bump packaging from 24.2 to 25.0. ([\#18393](https://github.com/element-hq/synapse/issues/18393))
|
||||
* Bump pillow from 11.1.0 to 11.2.1. ([\#18429](https://github.com/element-hq/synapse/issues/18429))
|
||||
* Bump pydantic from 2.10.3 to 2.11.4. ([\#18394](https://github.com/element-hq/synapse/issues/18394))
|
||||
* Bump pyo3-log from 0.12.2 to 0.12.3. ([\#18317](https://github.com/element-hq/synapse/issues/18317))
|
||||
* Bump pyopenssl from 24.3.0 to 25.0.0. ([\#18315](https://github.com/element-hq/synapse/issues/18315))
|
||||
* Bump sha2 from 0.10.8 to 0.10.9. ([\#18395](https://github.com/element-hq/synapse/issues/18395))
|
||||
* Bump sigstore/cosign-installer from 3.8.1 to 3.8.2. ([\#18366](https://github.com/element-hq/synapse/issues/18366))
|
||||
* Bump softprops/action-gh-release from 1 to 2. ([\#18264](https://github.com/element-hq/synapse/issues/18264))
|
||||
* Bump stefanzweifel/git-auto-commit-action from 5.1.0 to 5.2.0. ([\#18354](https://github.com/element-hq/synapse/issues/18354))
|
||||
* Bump txredisapi from 1.4.10 to 1.4.11. ([\#18392](https://github.com/element-hq/synapse/issues/18392))
|
||||
* Bump types-jsonschema from 4.23.0.20240813 to 4.23.0.20241208. ([\#18305](https://github.com/element-hq/synapse/issues/18305))
|
||||
* Bump types-psycopg2 from 2.9.21.20250121 to 2.9.21.20250318. ([\#18316](https://github.com/element-hq/synapse/issues/18316))
|
||||
|
||||
# Synapse 1.129.0 (2025-05-06)
|
||||
|
||||
No significant changes since 1.129.0rc2.
|
||||
|
||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -13,9 +13,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@@ -277,9 +277,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.23.5"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
|
||||
checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
@@ -296,9 +296,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.23.5"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
|
||||
checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
@@ -306,9 +306,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.23.5"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
|
||||
checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@@ -316,9 +316,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-log"
|
||||
version = "0.12.2"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b78e4983ba15bc62833a0e0941d965bc03690163f1127864f1408db25063466"
|
||||
checksum = "45192e5e4a4d2505587e27806c7b710c231c40c56f3bfc19535d0bb25df52264"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"log",
|
||||
@@ -327,9 +327,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.23.5"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
|
||||
checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@@ -339,9 +339,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.23.5"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
|
||||
checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -352,9 +352,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pythonize"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91a6ee7a084f913f98d70cdc3ebec07e852b735ae3059a1500db2661265da9ff"
|
||||
checksum = "d5bcac0d0b71821f0d69e42654f1e15e5c94b85196446c4de9588951a2117e7b"
|
||||
dependencies = [
|
||||
"pyo3",
|
||||
"serde",
|
||||
@@ -480,9 +480,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@@ -532,9 +532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.14"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
|
||||
12
README.rst
12
README.rst
@@ -253,15 +253,17 @@ Alongside all that, join our developer community on Matrix:
|
||||
Copyright and Licensing
|
||||
=======================
|
||||
|
||||
Copyright 2014-2017 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017-2025 New Vector Ltd
|
||||
| Copyright 2014-2017 OpenMarket Ltd
|
||||
| Copyright 2017 Vector Creations Ltd
|
||||
| Copyright 2017-2025 New Vector Ltd
|
||||
|
|
||||
|
||||
This software is dual-licensed by New Vector Ltd (Element). It can be used either:
|
||||
|
||||
|
||||
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
|
||||
|
||||
|
||||
(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
|
||||
|
||||
|
||||
|
||||
1
changelog.d/17892.doc
Normal file
1
changelog.d/17892.doc
Normal file
@@ -0,0 +1 @@
|
||||
Generate config documentation from JSON Schema file.
|
||||
1
changelog.d/18180.feature
Normal file
1
changelog.d/18180.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add `msc4263_limit_key_queries_to_users_who_share_rooms` config option as per [MSC4263](https://github.com/matrix-org/matrix-spec-proposals/pull/4263).
|
||||
1
changelog.d/18252.misc
Normal file
1
changelog.d/18252.misc
Normal file
@@ -0,0 +1 @@
|
||||
Mark dehydrated devices in the [List All User Devices Admin API](https://element-hq.github.io/synapse/latest/admin_api/user_admin_api.html#list-all-devices).
|
||||
1
changelog.d/18262.feature
Normal file
1
changelog.d/18262.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add option to allow registrations that begin with `_`. Contributed by `_` (@hex5f).
|
||||
1
changelog.d/18286.doc
Normal file
1
changelog.d/18286.doc
Normal file
@@ -0,0 +1 @@
|
||||
Update `room_list_publication_rules` docs to consider defaults that changed in v1.126.0. Contributed by @HarHarLinks.
|
||||
1
changelog.d/18318.feature
Normal file
1
changelog.d/18318.feature
Normal file
@@ -0,0 +1 @@
|
||||
Include room ID in room deletion status response.
|
||||
1
changelog.d/18380.misc
Normal file
1
changelog.d/18380.misc
Normal file
@@ -0,0 +1 @@
|
||||
Fix a memory leak in `_NotifierUserStream`.
|
||||
1
changelog.d/18387.feature
Normal file
1
changelog.d/18387.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add support for calling Policy Servers ([MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284)) to mark events as spam.
|
||||
1
changelog.d/18391.bugfix
Normal file
1
changelog.d/18391.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Prevent race-condition in `_maybe_retry_device_resync` entrance.
|
||||
1
changelog.d/18409.misc
Normal file
1
changelog.d/18409.misc
Normal file
@@ -0,0 +1 @@
|
||||
Fix a couple type annotations in the `RootConfig`/`Config`.
|
||||
1
changelog.d/18417.misc
Normal file
1
changelog.d/18417.misc
Normal file
@@ -0,0 +1 @@
|
||||
Explicitly enable PyPy builds in `cibuildwheel`s config to avoid it being disabled on a future upgrade to `cibuildwheel` v3.
|
||||
1
changelog.d/18419.misc
Normal file
1
changelog.d/18419.misc
Normal file
@@ -0,0 +1 @@
|
||||
Update the PR review template to remove an erroneous line break from the final bullet point.
|
||||
1
changelog.d/18420.misc
Normal file
1
changelog.d/18420.misc
Normal file
@@ -0,0 +1 @@
|
||||
Explain why we `flush_buffer()` for Python `print(...)` output.
|
||||
1
changelog.d/18430.bugfix
Normal file
1
changelog.d/18430.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix the `tests.handlers.test_worker_lock.WorkerLockTestCase.test_lock_contention` test which could spuriously time out on RISC-V architectures due to performance differences.
|
||||
1
changelog.d/18434.bugfix
Normal file
1
changelog.d/18434.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix admin redaction endpoint not redacting encrypted messages.
|
||||
1
changelog.d/18440.misc
Normal file
1
changelog.d/18440.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add lint to ensure we don't add a `CREATE/DROP INDEX` in a schema delta.
|
||||
1
changelog.d/18445.doc
Normal file
1
changelog.d/18445.doc
Normal file
@@ -0,0 +1 @@
|
||||
Add advice for upgrading between major PostgreSQL versions to the database documentation.
|
||||
1
changelog.d/18451.misc
Normal file
1
changelog.d/18451.misc
Normal file
@@ -0,0 +1 @@
|
||||
Bump ruff from 0.7.3 to 0.11.10.
|
||||
1
changelog.d/18454.misc
Normal file
1
changelog.d/18454.misc
Normal file
@@ -0,0 +1 @@
|
||||
Allow checking only for the existence of a field in an SSO provider's response, rather than requiring the value(s) to check.
|
||||
1
changelog.d/18459.misc
Normal file
1
changelog.d/18459.misc
Normal file
@@ -0,0 +1 @@
|
||||
Bump tornado from 6.4.2 to 6.5.0.
|
||||
1
changelog.d/18460.misc
Normal file
1
changelog.d/18460.misc
Normal file
@@ -0,0 +1 @@
|
||||
Bump pyo3 from 0.23.5 to 0.24.2.
|
||||
1
changelog.d/18463.misc
Normal file
1
changelog.d/18463.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add unit tests for homeserver usage statistics.
|
||||
1
changelog.d/18471.misc
Normal file
1
changelog.d/18471.misc
Normal file
@@ -0,0 +1 @@
|
||||
Don't move invited users to new room when shutting down room.
|
||||
12
debian/changelog
vendored
12
debian/changelog
vendored
@@ -1,3 +1,15 @@
|
||||
matrix-synapse-py3 (1.130.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.130.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 20 May 2025 08:34:13 -0600
|
||||
|
||||
matrix-synapse-py3 (1.130.0~rc1) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.130.0rc1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 13 May 2025 10:44:04 +0100
|
||||
|
||||
matrix-synapse-py3 (1.129.0) stable; urgency=medium
|
||||
|
||||
* New Synapse release 1.129.0.
|
||||
|
||||
@@ -3,18 +3,37 @@
|
||||
ARG SYNAPSE_VERSION=latest
|
||||
ARG FROM=matrixdotorg/synapse:$SYNAPSE_VERSION
|
||||
ARG DEBIAN_VERSION=bookworm
|
||||
ARG PYTHON_VERSION=3.12
|
||||
|
||||
# first of all, we create a base image with an nginx which we can copy into the
|
||||
# first of all, we create a base image with dependencies which we can copy into the
|
||||
# target image. For repeated rebuilds, this is much faster than apt installing
|
||||
# each time.
|
||||
|
||||
FROM docker.io/library/debian:${DEBIAN_VERSION}-slim AS deps_base
|
||||
FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-${DEBIAN_VERSION} AS deps_base
|
||||
|
||||
# Tell apt to keep downloaded package files, as we're using cache mounts.
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update -qq && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
|
||||
redis-server nginx-light
|
||||
nginx-light
|
||||
|
||||
RUN \
|
||||
# remove default page
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
# have nginx log to stderr/out
|
||||
ln -sf /dev/stdout /var/log/nginx/access.log && \
|
||||
ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
|
||||
# --link-mode=copy silences a warning as uv isn't able to do hardlinks between its cache
|
||||
# (mounted as --mount=type=cache) and the target directory.
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install --link-mode=copy --prefix="/uv/usr/local" supervisor~=4.2
|
||||
|
||||
RUN mkdir -p /uv/etc/supervisor/conf.d
|
||||
|
||||
# Similarly, a base to copy the redis server from.
|
||||
#
|
||||
@@ -27,31 +46,16 @@ FROM docker.io/library/redis:7-${DEBIAN_VERSION} AS redis_base
|
||||
# now build the final image, based on the the regular Synapse docker image
|
||||
FROM $FROM
|
||||
|
||||
# Install supervisord with uv pip instead of apt, to avoid installing a second
|
||||
# copy of python.
|
||||
# --link-mode=copy silences a warning as uv isn't able to do hardlinks between its cache
|
||||
# (mounted as --mount=type=cache) and the target directory.
|
||||
RUN \
|
||||
--mount=type=bind,from=ghcr.io/astral-sh/uv:0.6.8,source=/uv,target=/uv \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
/uv pip install --link-mode=copy --prefix="/usr/local" supervisor~=4.2
|
||||
|
||||
RUN mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# Copy over redis and nginx
|
||||
# Copy over dependencies
|
||||
COPY --from=redis_base /usr/local/bin/redis-server /usr/local/bin
|
||||
|
||||
COPY --from=deps_base /uv /
|
||||
COPY --from=deps_base /usr/sbin/nginx /usr/sbin
|
||||
COPY --from=deps_base /usr/share/nginx /usr/share/nginx
|
||||
COPY --from=deps_base /usr/lib/nginx /usr/lib/nginx
|
||||
COPY --from=deps_base /etc/nginx /etc/nginx
|
||||
RUN rm /etc/nginx/sites-enabled/default
|
||||
RUN mkdir /var/log/nginx /var/lib/nginx
|
||||
RUN chown www-data /var/lib/nginx
|
||||
|
||||
# have nginx log to stderr/out
|
||||
RUN ln -sf /dev/stdout /var/log/nginx/access.log
|
||||
RUN ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
COPY --from=deps_base /var/log/nginx /var/log/nginx
|
||||
# chown to allow non-root user to write to http-*-temp-path dirs
|
||||
COPY --from=deps_base --chown=www-data:root /var/lib/nginx /var/lib/nginx
|
||||
|
||||
# Copy Synapse worker, nginx and supervisord configuration template files
|
||||
COPY ./docker/conf-workers/* /conf/
|
||||
@@ -70,4 +74,4 @@ FROM $FROM
|
||||
# Replace the healthcheck with one which checks *all* the workers. The script
|
||||
# is generated by configure_workers_and_start.py.
|
||||
HEALTHCHECK --start-period=5s --interval=15s --timeout=5s \
|
||||
CMD /bin/sh /healthcheck.sh
|
||||
CMD ["/healthcheck.sh"]
|
||||
|
||||
@@ -58,4 +58,4 @@ ENTRYPOINT ["/start_for_complement.sh"]
|
||||
|
||||
# Update the healthcheck to have a shorter check interval
|
||||
HEALTHCHECK --start-period=5s --interval=1s --timeout=1s \
|
||||
CMD /bin/sh /healthcheck.sh
|
||||
CMD ["/healthcheck.sh"]
|
||||
|
||||
@@ -9,7 +9,7 @@ echo " Args: $*"
|
||||
echo " Env: SYNAPSE_COMPLEMENT_DATABASE=$SYNAPSE_COMPLEMENT_DATABASE SYNAPSE_COMPLEMENT_USE_WORKERS=$SYNAPSE_COMPLEMENT_USE_WORKERS SYNAPSE_COMPLEMENT_USE_ASYNCIO_REACTOR=$SYNAPSE_COMPLEMENT_USE_ASYNCIO_REACTOR"
|
||||
|
||||
function log {
|
||||
d=$(date +"%Y-%m-%d %H:%M:%S,%3N")
|
||||
d=$(printf '%(%Y-%m-%d %H:%M:%S)T,%.3s\n' ${EPOCHREALTIME/./ })
|
||||
echo "$d $*"
|
||||
}
|
||||
|
||||
@@ -103,12 +103,11 @@ fi
|
||||
# Note that both the key and certificate are in PEM format (not DER).
|
||||
|
||||
# First generate a configuration file to set up a Subject Alternative Name.
|
||||
cat > /conf/server.tls.conf <<EOF
|
||||
echo "\
|
||||
.include /etc/ssl/openssl.cnf
|
||||
|
||||
[SAN]
|
||||
subjectAltName=DNS:${SERVER_NAME}
|
||||
EOF
|
||||
subjectAltName=DNS:${SERVER_NAME}" > /conf/server.tls.conf
|
||||
|
||||
# Generate an RSA key
|
||||
openssl genrsa -out /conf/server.tls.key 2048
|
||||
@@ -123,8 +122,8 @@ openssl x509 -req -in /conf/server.tls.csr \
|
||||
-out /conf/server.tls.crt -extfile /conf/server.tls.conf -extensions SAN
|
||||
|
||||
# Assert that we have a Subject Alternative Name in the certificate.
|
||||
# (grep will exit with 1 here if there isn't a SAN in the certificate.)
|
||||
openssl x509 -in /conf/server.tls.crt -noout -text | grep DNS:
|
||||
# (the test will exit with 1 here if there isn't a SAN in the certificate.)
|
||||
[[ $(openssl x509 -in /conf/server.tls.crt -noout -text) == *DNS:* ]]
|
||||
|
||||
export SYNAPSE_TLS_CERT=/conf/server.tls.crt
|
||||
export SYNAPSE_TLS_KEY=/conf/server.tls.key
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/local/bin/python
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
@@ -202,6 +202,7 @@ WORKERS_CONFIG: Dict[str, Dict[str, Any]] = {
|
||||
"app": "synapse.app.generic_worker",
|
||||
"listener_resources": ["federation"],
|
||||
"endpoint_patterns": [
|
||||
"^/_matrix/federation/v1/version$",
|
||||
"^/_matrix/federation/(v1|v2)/event/",
|
||||
"^/_matrix/federation/(v1|v2)/state/",
|
||||
"^/_matrix/federation/(v1|v2)/state_ids/",
|
||||
@@ -351,6 +352,11 @@ def error(txt: str) -> NoReturn:
|
||||
|
||||
|
||||
def flush_buffers() -> None:
|
||||
"""
|
||||
Python's `print()` buffers output by default, typically waiting until ~8KB
|
||||
accumulates. This method can be used to flush the buffers so we can see the output
|
||||
of any print statements so far.
|
||||
"""
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
@@ -376,9 +382,11 @@ def convert(src: str, dst: str, **template_vars: object) -> None:
|
||||
#
|
||||
# We use append mode in case the files have already been written to by something else
|
||||
# (for instance, as part of the instructions in a dockerfile).
|
||||
exists = os.path.isfile(dst)
|
||||
with open(dst, "a") as outfile:
|
||||
# In case the existing file doesn't end with a newline
|
||||
outfile.write("\n")
|
||||
if exists:
|
||||
outfile.write("\n")
|
||||
|
||||
outfile.write(rendered)
|
||||
|
||||
@@ -604,7 +612,7 @@ def generate_base_homeserver_config() -> None:
|
||||
# start.py already does this for us, so just call that.
|
||||
# note that this script is copied in in the official, monolith dockerfile
|
||||
os.environ["SYNAPSE_HTTP_PORT"] = str(MAIN_PROCESS_HTTP_LISTENER_PORT)
|
||||
subprocess.run(["/usr/local/bin/python", "/start.py", "migrate_config"], check=True)
|
||||
subprocess.run([sys.executable, "/start.py", "migrate_config"], check=True)
|
||||
|
||||
|
||||
def parse_worker_types(
|
||||
@@ -998,6 +1006,7 @@ def generate_worker_files(
|
||||
"/healthcheck.sh",
|
||||
healthcheck_urls=healthcheck_urls,
|
||||
)
|
||||
os.chmod("/healthcheck.sh", 0o755)
|
||||
|
||||
# Ensure the logging directory exists
|
||||
log_dir = data_dir + "/logs"
|
||||
|
||||
@@ -22,6 +22,11 @@ def error(txt: str) -> NoReturn:
|
||||
|
||||
|
||||
def flush_buffers() -> None:
|
||||
"""
|
||||
Python's `print()` buffers output by default, typically waiting until ~8KB
|
||||
accumulates. This method can be used to flush the buffers so we can see the output
|
||||
of any print statements so far.
|
||||
"""
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
@@ -63,6 +63,18 @@ mdbook serve
|
||||
|
||||
The URL at which the docs can be viewed at will be logged.
|
||||
|
||||
## Synapse configuration documentation
|
||||
|
||||
The [Configuration
|
||||
Manual](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html)
|
||||
page is generated from a YAML file,
|
||||
[schema/synapse-config.schema.yaml](../schema/synapse-config.schema.yaml). To
|
||||
add new options or modify existing ones, first edit that file, then run
|
||||
[scripts-dev/gen_config_documentation.py](../scripts-dev/gen_config_documentation.py)
|
||||
to generate an updated Configuration Manual markdown file.
|
||||
|
||||
Build the book as described above to preview it in a web browser.
|
||||
|
||||
## Configuration and theming
|
||||
|
||||
The look and behaviour of the website is configured by the [book.toml](../book.toml) file
|
||||
|
||||
@@ -794,6 +794,7 @@ A response body like the following is returned:
|
||||
"results": [
|
||||
{
|
||||
"delete_id": "delete_id1",
|
||||
"room_id": "!roomid:example.com",
|
||||
"status": "failed",
|
||||
"error": "error message",
|
||||
"shutdown_room": {
|
||||
@@ -804,6 +805,7 @@ A response body like the following is returned:
|
||||
}
|
||||
}, {
|
||||
"delete_id": "delete_id2",
|
||||
"room_id": "!roomid:example.com",
|
||||
"status": "purging",
|
||||
"shutdown_room": {
|
||||
"kicked_users": [
|
||||
@@ -842,6 +844,8 @@ A response body like the following is returned:
|
||||
```json
|
||||
{
|
||||
"status": "purging",
|
||||
"delete_id": "bHkCNQpHqOaFhPtK",
|
||||
"room_id": "!roomid:example.com",
|
||||
"shutdown_room": {
|
||||
"kicked_users": [
|
||||
"@foobar:example.com"
|
||||
@@ -869,7 +873,8 @@ The following fields are returned in the JSON response body:
|
||||
- `results` - An array of objects, each containing information about one task.
|
||||
This field is omitted from the result when you query by `delete_id`.
|
||||
Task objects contain the following fields:
|
||||
- `delete_id` - The ID for this purge if you query by `room_id`.
|
||||
- `delete_id` - The ID for this purge
|
||||
- `room_id` - The ID of the room being deleted
|
||||
- `status` - The status will be one of:
|
||||
- `shutting_down` - The process is removing users from the room.
|
||||
- `purging` - The process is purging the room and event data from database.
|
||||
|
||||
54
docs/admin_api/scheduled_tasks.md
Normal file
54
docs/admin_api/scheduled_tasks.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Show scheduled tasks
|
||||
|
||||
This API returns information about scheduled tasks.
|
||||
|
||||
To use it, you will need to authenticate by providing an `access_token`
|
||||
for a server admin: see [Admin API](../usage/administration/admin_api/).
|
||||
|
||||
The api is:
|
||||
```
|
||||
GET /_synapse/admin/v1/scheduled_tasks
|
||||
```
|
||||
|
||||
It returns a JSON body like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"scheduled_tasks": [
|
||||
{
|
||||
"id": "GSA124oegf1",
|
||||
"action": "shutdown_room",
|
||||
"status": "complete",
|
||||
"timestamp_ms": 23423523,
|
||||
"resource_id": "!roomid",
|
||||
"result": "some result",
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
* `action_name`: string - Is optional. Returns only the scheduled tasks with the given action name.
|
||||
* `resource_id`: string - Is optional. Returns only the scheduled tasks with the given resource id.
|
||||
* `status`: string - Is optional. Returns only the scheduled tasks matching the given status, one of
|
||||
- "scheduled" - Task is scheduled but not active
|
||||
- "active" - Task is active and probably running, and if not will be run on next scheduler loop run
|
||||
- "complete" - Task has completed successfully
|
||||
- "failed" - Task is over and either returned a failed status, or had an exception
|
||||
|
||||
* `max_timestamp`: int - Is optional. Returns only the scheduled tasks with a timestamp inferior to the specified one.
|
||||
|
||||
**Response**
|
||||
|
||||
The following fields are returned in the JSON response body along with a `200` HTTP status code:
|
||||
|
||||
* `id`: string - ID of scheduled task.
|
||||
* `action`: string - The name of the scheduled task's action.
|
||||
* `status`: string - The status of the scheduled task.
|
||||
* `timestamp_ms`: integer - The timestamp (in milliseconds since the unix epoch) of the given task - If the status is "scheduled" then this represents when it should be launched.
|
||||
Otherwise it represents the last time this task got a change of state.
|
||||
* `resource_id`: Optional string - The resource id of the scheduled task, if it possesses one
|
||||
* `result`: Optional Json - Any result of the scheduled task, if given
|
||||
* `error`: Optional string - If the task has the status "failed", the error associated with this failure
|
||||
@@ -954,7 +954,8 @@ A response body like the following is returned:
|
||||
"last_seen_ip": "1.2.3.4",
|
||||
"last_seen_user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0",
|
||||
"last_seen_ts": 1474491775024,
|
||||
"user_id": "<user_id>"
|
||||
"user_id": "<user_id>",
|
||||
"dehydrated": false
|
||||
},
|
||||
{
|
||||
"device_id": "AUIECTSRND",
|
||||
@@ -962,7 +963,8 @@ A response body like the following is returned:
|
||||
"last_seen_ip": "1.2.3.5",
|
||||
"last_seen_user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0",
|
||||
"last_seen_ts": 1474491775025,
|
||||
"user_id": "<user_id>"
|
||||
"user_id": "<user_id>",
|
||||
"dehydrated": false
|
||||
}
|
||||
],
|
||||
"total": 2
|
||||
@@ -992,6 +994,7 @@ The following fields are returned in the JSON response body:
|
||||
- `last_seen_ts` - The timestamp (in milliseconds since the unix epoch) when this
|
||||
devices was last seen. (May be a few minutes out of date, for efficiency reasons).
|
||||
- `user_id` - Owner of device.
|
||||
- `dehydrated` - Whether the device is a dehydrated device.
|
||||
|
||||
- `total` - Total number of user's devices.
|
||||
|
||||
|
||||
@@ -353,6 +353,8 @@ callback returns `False`, Synapse falls through to the next one. The value of th
|
||||
callback that does not return `False` will be used. If this happens, Synapse will not call
|
||||
any of the subsequent implementations of this callback.
|
||||
|
||||
Note that this check is applied to federation invites as of Synapse v1.130.0.
|
||||
|
||||
|
||||
### `check_login_for_spam`
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ such as [Github][github-idp].
|
||||
[auth0]: https://auth0.com/
|
||||
[authentik]: https://goauthentik.io/
|
||||
[lemonldap]: https://lemonldap-ng.org/
|
||||
[pocket-id]: https://pocket-id.org/
|
||||
[okta]: https://www.okta.com/
|
||||
[dex-idp]: https://github.com/dexidp/dex
|
||||
[keycloak-idp]: https://www.keycloak.org/docs/latest/server_admin/#sso-protocols
|
||||
@@ -624,6 +625,32 @@ oidc_providers:
|
||||
|
||||
Note that the fields `client_id` and `client_secret` are taken from the CURL response above.
|
||||
|
||||
### Pocket ID
|
||||
|
||||
[Pocket ID][pocket-id] is a simple OIDC provider that allows users to authenticate with their passkeys.
|
||||
1. Go to `OIDC Clients`
|
||||
2. Click on `Add OIDC Client`
|
||||
3. Add a name, for example `Synapse`
|
||||
4. Add `"https://auth.example.org/_synapse/client/oidc/callback` to `Callback URLs` # Replace `auth.example.org` with your domain
|
||||
5. Click on `Save`
|
||||
6. Note down your `Client ID` and `Client secret`, these will be used later
|
||||
|
||||
Synapse config:
|
||||
|
||||
```yaml
|
||||
oidc_providers:
|
||||
- idp_id: pocket_id
|
||||
idp_name: Pocket ID
|
||||
issuer: "https://auth.example.org/" # Replace with your domain
|
||||
client_id: "your-client-id" # Replace with the "Client ID" you noted down before
|
||||
client_secret: "your-client-secret" # Replace with the "Client secret" you noted down before
|
||||
scopes: ["openid", "profile"]
|
||||
user_mapping_provider:
|
||||
config:
|
||||
localpart_template: "{{ user.preferred_username }}"
|
||||
display_name_template: "{{ user.name }}"
|
||||
```
|
||||
|
||||
### Shibboleth with OIDC Plugin
|
||||
|
||||
[Shibboleth](https://www.shibboleth.net/) is an open Standard IdP solution widely used by Universities.
|
||||
|
||||
@@ -100,6 +100,14 @@ database:
|
||||
keepalives_count: 3
|
||||
```
|
||||
|
||||
## Postgresql major version upgrades
|
||||
|
||||
Postgres uses separate directories for database locations between major versions (typically `/var/lib/postgresql/<version>/main`).
|
||||
|
||||
Therefore, it is recommended to stop Synapse and other services (MAS, etc) before upgrading Postgres major versions.
|
||||
|
||||
It is also strongly recommended to [back up](./usage/administration/backups.md#database) your database beforehand to ensure no data loss arising from a failed upgrade.
|
||||
|
||||
## Backups
|
||||
|
||||
Don't forget to [back up](./usage/administration/backups.md#database) your database!
|
||||
|
||||
@@ -117,6 +117,16 @@ each upgrade are complete before moving on to the next upgrade, to avoid
|
||||
stacking them up. You can monitor the currently running background updates with
|
||||
[the Admin API](usage/administration/admin_api/background_updates.html#status).
|
||||
|
||||
# Upgrading to v1.130.0
|
||||
|
||||
## Documented endpoint which can be delegated to a federation worker
|
||||
|
||||
The endpoint `^/_matrix/federation/v1/version$` can be delegated to a federation
|
||||
worker. This is not new behaviour, but had not been documented yet. The
|
||||
[list of delegatable endpoints](workers.md#synapseappgeneric_worker) has
|
||||
been updated to include it. Make sure to check your reverse proxy rules if you
|
||||
are using workers.
|
||||
|
||||
# Upgrading to v1.126.0
|
||||
|
||||
## Room list publication rules change
|
||||
|
||||
@@ -255,7 +255,7 @@ line to `/etc/default/matrix-synapse`:
|
||||
|
||||
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
|
||||
|
||||
*Note*: You may need to set `PYTHONMALLOC=malloc` to ensure that `jemalloc` can accurately calculate memory usage. By default, Python uses its internal small-object allocator, which may interfere with jemalloc's ability to track memory consumption correctly. This could prevent the [cache_autotuning](../configuration/config_documentation.md#caches-and-associated-values) feature from functioning as expected, as the Python allocator may not reach the memory threshold set by `max_cache_memory_usage`, thus not triggering the cache eviction process.
|
||||
*Note*: You may need to set `PYTHONMALLOC=malloc` to ensure that `jemalloc` can accurately calculate memory usage. By default, Python uses its internal small-object allocator, which may interfere with jemalloc's ability to track memory consumption correctly. This could prevent the [cache_autotuning](../configuration/config_documentation.md#caches) feature from functioning as expected, as the Python allocator may not reach the memory threshold set by `max_cache_memory_usage`, thus not triggering the cache eviction process.
|
||||
|
||||
This made a significant difference on Python 2.7 - it's unclear how
|
||||
much of an improvement it provides on Python 3.x.
|
||||
|
||||
@@ -30,7 +30,7 @@ The following statistics are sent to the configured reporting endpoint:
|
||||
| `python_version` | string | The Python version number in use (e.g "3.7.1"). Taken from `sys.version_info`. |
|
||||
| `total_users` | int | The number of registered users on the homeserver. |
|
||||
| `total_nonbridged_users` | int | The number of users, excluding those created by an Application Service. |
|
||||
| `daily_user_type_native` | int | The number of native users created in the last 24 hours. |
|
||||
| `daily_user_type_native` | int | The number of native, non-guest users created in the last 24 hours. |
|
||||
| `daily_user_type_guest` | int | The number of guest users created in the last 24 hours. |
|
||||
| `daily_user_type_bridged` | int | The number of users created by Application Services in the last 24 hours. |
|
||||
| `total_room_count` | int | The total number of rooms present on the homeserver. |
|
||||
@@ -50,8 +50,8 @@ The following statistics are sent to the configured reporting endpoint:
|
||||
| `cache_factor` | int | The configured [`global factor`](../../configuration/config_documentation.md#caching) value for caching. |
|
||||
| `event_cache_size` | int | The configured [`event_cache_size`](../../configuration/config_documentation.md#caching) value for caching. |
|
||||
| `database_engine` | string | The database engine that is in use. Either "psycopg2" meaning PostgreSQL is in use, or "sqlite3" for SQLite3. |
|
||||
| `database_server_version` | string | The version of the database server. Examples being "10.10" for PostgreSQL server version 10.0, and "3.38.5" for SQLite 3.38.5 installed on the system. |
|
||||
| `log_level` | string | The log level in use. Examples are "INFO", "WARNING", "ERROR", "DEBUG", etc. |
|
||||
| `database_server_version` | string | The version of the database server. Examples being "10.10" for PostgreSQL server version 10.0, and "3.38.5" for SQLite 3.38.5 installed on the system. |
|
||||
| `log_level` | string | The log level in use. Examples are "INFO", "WARNING", "ERROR", "DEBUG", etc. |
|
||||
|
||||
|
||||
[^1]: Native matrix users and guests are always counted. If the
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,7 @@ information.
|
||||
^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$
|
||||
|
||||
# Federation requests
|
||||
^/_matrix/federation/v1/version$
|
||||
^/_matrix/federation/v1/event/
|
||||
^/_matrix/federation/v1/state/
|
||||
^/_matrix/federation/v1/state_ids/
|
||||
@@ -249,6 +250,7 @@ information.
|
||||
^/_matrix/client/(api/v1|r0|v3|unstable)/directory/room/.*$
|
||||
^/_matrix/client/(r0|v3|unstable)/capabilities$
|
||||
^/_matrix/client/(r0|v3|unstable)/notifications$
|
||||
^/_synapse/admin/v1/rooms/
|
||||
|
||||
# Encryption requests
|
||||
^/_matrix/client/(r0|v3|unstable)/keys/query$
|
||||
@@ -280,6 +282,7 @@ Additionally, the following REST endpoints can be handled for GET requests:
|
||||
|
||||
^/_matrix/client/(api/v1|r0|v3|unstable)/pushrules/
|
||||
^/_matrix/client/unstable/org.matrix.msc4140/delayed_events
|
||||
^/_matrix/client/(api/v1|r0|v3|unstable)/devices/
|
||||
|
||||
# Account data requests
|
||||
^/_matrix/client/(r0|v3|unstable)/.*/tags
|
||||
@@ -320,6 +323,15 @@ For multiple workers not handling the SSO endpoints properly, see
|
||||
[#7530](https://github.com/matrix-org/synapse/issues/7530) and
|
||||
[#9427](https://github.com/matrix-org/synapse/issues/9427).
|
||||
|
||||
Additionally, when MSC3861 is enabled (`experimental_features.msc3861.enabled`
|
||||
set to `true`), the following endpoints can be handled by the worker:
|
||||
|
||||
^/_synapse/admin/v2/users/[^/]+$
|
||||
^/_synapse/admin/v1/username_available$
|
||||
^/_synapse/admin/v1/users/[^/]+/_allow_cross_signing_replacement_without_uia$
|
||||
# Only the GET method:
|
||||
^/_synapse/admin/v1/users/[^/]+/devices$
|
||||
|
||||
Note that a [HTTP listener](usage/configuration/config_documentation.md#listeners)
|
||||
with `client` and `federation` `resources` must be configured in the
|
||||
[`worker_listeners`](usage/configuration/config_documentation.md#worker_listeners)
|
||||
|
||||
804
poetry.lock
generated
804
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ module-name = "synapse.synapse_rust"
|
||||
|
||||
[tool.poetry]
|
||||
name = "matrix-synapse"
|
||||
version = "1.129.0"
|
||||
version = "1.130.0"
|
||||
description = "Homeserver for the Matrix decentralised comms protocol"
|
||||
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
@@ -320,7 +320,7 @@ all = [
|
||||
# failing on new releases. Keeping lower bounds loose here means that dependabot
|
||||
# can bump versions without having to update the content-hash in the lockfile.
|
||||
# This helps prevents merge conflicts when running a batch of dependabot updates.
|
||||
ruff = "0.7.3"
|
||||
ruff = "0.11.11"
|
||||
# Type checking only works with the pydantic.v1 compat module from pydantic v2
|
||||
pydantic = "^2"
|
||||
|
||||
@@ -385,6 +385,9 @@ build-backend = "poetry.core.masonry.api"
|
||||
# - PyPy on Aarch64 and musllinux on aarch64: too slow to build.
|
||||
# c.f. https://github.com/matrix-org/synapse/pull/14259
|
||||
skip = "cp36* cp37* cp38* pp37* pp38* *-musllinux_i686 pp*aarch64 *-musllinux_aarch64"
|
||||
# Enable non-default builds.
|
||||
# "pypy" used to be included by default up until cibuildwheel 3.
|
||||
enable = "pypy"
|
||||
|
||||
# We need a rust compiler.
|
||||
#
|
||||
|
||||
@@ -30,14 +30,14 @@ http = "1.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.17"
|
||||
mime = "0.3.17"
|
||||
pyo3 = { version = "0.23.5", features = [
|
||||
pyo3 = { version = "0.24.2", features = [
|
||||
"macros",
|
||||
"anyhow",
|
||||
"abi3",
|
||||
"abi3-py39",
|
||||
] }
|
||||
pyo3-log = "0.12.0"
|
||||
pythonize = "0.23.0"
|
||||
pythonize = "0.24.0"
|
||||
regex = "1.6.0"
|
||||
sha2 = "0.10.8"
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
|
||||
5711
schema/synapse-config.schema.yaml
Normal file
5711
schema/synapse-config.schema.yaml
Normal file
File diff suppressed because it is too large
Load Diff
2
schema/v1/Do not edit files in this folder
Normal file
2
schema/v1/Do not edit files in this folder
Normal file
@@ -0,0 +1,2 @@
|
||||
If you want to update the meta schema, copy this folder and increase its version
|
||||
number instead.
|
||||
29
schema/v1/meta.schema.json
Normal file
29
schema/v1/meta.schema.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://element-hq.github.io/synapse/latest/schema/v1/meta.schema.json",
|
||||
"$vocabulary": {
|
||||
"https://json-schema.org/draft/2020-12/vocab/core": true,
|
||||
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
|
||||
"https://json-schema.org/draft/2020-12/vocab/unevaluated": true,
|
||||
"https://json-schema.org/draft/2020-12/vocab/validation": true,
|
||||
"https://json-schema.org/draft/2020-12/vocab/meta-data": true,
|
||||
"https://json-schema.org/draft/2020-12/vocab/format-annotation": true,
|
||||
"https://json-schema.org/draft/2020-12/vocab/content": true,
|
||||
"https://element-hq.github.io/synapse/latest/schema/v1/vocab/documentation": false
|
||||
},
|
||||
"$ref": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"io.element.type_name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable type of a schema that is displayed instead of the standard JSON Schema types like `object` or `integer`. In case the JSON Schema type contains `null`, this information should be presented alongside the human-readable type name.",
|
||||
"examples": ["duration", "byte size"]
|
||||
},
|
||||
"io.element.post_description": {
|
||||
"type": "string",
|
||||
"description": "Additional description of a schema, better suited to be placed less prominently in the generated documentation, e.g., at the end of a section after listings of items and properties.",
|
||||
"examples": [
|
||||
"### Advanced uses\n\nThe spent coffee grounds can be added to compost for improving soil and growing plants."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
schema/v1/vocab/documentation.html
Normal file
11
schema/v1/vocab/documentation.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; URL=../meta.schema.json">
|
||||
<meta charset="UTF-8">
|
||||
<title>Redirecting to ../meta.schema.json…</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="../meta.schema.json">../meta.schema.json</a>…</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Check that no schema deltas have been added to the wrong version.
|
||||
#
|
||||
# Also checks that schema deltas do not try and create or drop indices.
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
@@ -9,6 +11,13 @@ import click
|
||||
import git
|
||||
|
||||
SCHEMA_FILE_REGEX = re.compile(r"^synapse/storage/schema/(.*)/delta/(.*)/(.*)$")
|
||||
INDEX_CREATION_REGEX = re.compile(r"CREATE .*INDEX .*ON ([a-z_]+)", flags=re.IGNORECASE)
|
||||
INDEX_DELETION_REGEX = re.compile(r"DROP .*INDEX ([a-z_]+)", flags=re.IGNORECASE)
|
||||
TABLE_CREATION_REGEX = re.compile(r"CREATE .*TABLE ([a-z_]+)", flags=re.IGNORECASE)
|
||||
|
||||
# The base branch we want to check against. We use the main development branch
|
||||
# on the assumption that is what we are developing against.
|
||||
DEVELOP_BRANCH = "develop"
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -20,6 +29,9 @@ SCHEMA_FILE_REGEX = re.compile(r"^synapse/storage/schema/(.*)/delta/(.*)/(.*)$")
|
||||
help="Always output ANSI colours",
|
||||
)
|
||||
def main(force_colors: bool) -> None:
|
||||
# Return code. Set to non-zero when we encounter an error
|
||||
return_code = 0
|
||||
|
||||
click.secho(
|
||||
"+++ Checking schema deltas are in the right folder",
|
||||
fg="green",
|
||||
@@ -30,17 +42,17 @@ def main(force_colors: bool) -> None:
|
||||
click.secho("Updating repo...")
|
||||
|
||||
repo = git.Repo()
|
||||
repo.remote().fetch()
|
||||
repo.remote().fetch(refspec=DEVELOP_BRANCH)
|
||||
|
||||
click.secho("Getting current schema version...")
|
||||
|
||||
r = repo.git.show("origin/develop:synapse/storage/schema/__init__.py")
|
||||
r = repo.git.show(f"origin/{DEVELOP_BRANCH}:synapse/storage/schema/__init__.py")
|
||||
|
||||
locals: Dict[str, Any] = {}
|
||||
exec(r, locals)
|
||||
current_schema_version = locals["SCHEMA_VERSION"]
|
||||
|
||||
diffs: List[git.Diff] = repo.remote().refs.develop.commit.diff(None)
|
||||
diffs: List[git.Diff] = repo.remote().refs[DEVELOP_BRANCH].commit.diff(None)
|
||||
|
||||
# Get the schema version of the local file to check against current schema on develop
|
||||
with open("synapse/storage/schema/__init__.py") as file:
|
||||
@@ -53,7 +65,7 @@ def main(force_colors: bool) -> None:
|
||||
# local schema version must be +/-1 the current schema version on develop
|
||||
if abs(local_schema_version - current_schema_version) != 1:
|
||||
click.secho(
|
||||
"The proposed schema version has diverged more than one version from develop, please fix!",
|
||||
f"The proposed schema version has diverged more than one version from {DEVELOP_BRANCH}, please fix!",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
@@ -67,21 +79,28 @@ def main(force_colors: bool) -> None:
|
||||
click.secho(f"Current schema version: {current_schema_version}")
|
||||
|
||||
seen_deltas = False
|
||||
bad_files = []
|
||||
bad_delta_files = []
|
||||
changed_delta_files = []
|
||||
for diff in diffs:
|
||||
if not diff.new_file or diff.b_path is None:
|
||||
if diff.b_path is None:
|
||||
# We don't lint deleted files.
|
||||
continue
|
||||
|
||||
match = SCHEMA_FILE_REGEX.match(diff.b_path)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
changed_delta_files.append(diff.b_path)
|
||||
|
||||
if not diff.new_file:
|
||||
continue
|
||||
|
||||
seen_deltas = True
|
||||
|
||||
_, delta_version, _ = match.groups()
|
||||
|
||||
if delta_version != str(current_schema_version):
|
||||
bad_files.append(diff.b_path)
|
||||
bad_delta_files.append(diff.b_path)
|
||||
|
||||
if not seen_deltas:
|
||||
click.secho(
|
||||
@@ -92,41 +111,91 @@ def main(force_colors: bool) -> None:
|
||||
)
|
||||
return
|
||||
|
||||
if not bad_files:
|
||||
if bad_delta_files:
|
||||
bad_delta_files.sort()
|
||||
|
||||
click.secho(
|
||||
"Found deltas in the wrong folder!",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
|
||||
for f in bad_delta_files:
|
||||
click.secho(
|
||||
f"\t{f}",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
|
||||
click.secho()
|
||||
click.secho(
|
||||
f"Please move these files to delta/{current_schema_version}/",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
|
||||
else:
|
||||
click.secho(
|
||||
f"All deltas are in the correct folder: {current_schema_version}!",
|
||||
fg="green",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
return
|
||||
|
||||
bad_files.sort()
|
||||
# Make sure we process them in order. This sort works because deltas are numbered
|
||||
# and delta files are also numbered in order.
|
||||
changed_delta_files.sort()
|
||||
|
||||
click.secho(
|
||||
"Found deltas in the wrong folder!",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
# Now check that we're not trying to create or drop indices. If we want to
|
||||
# do that they should be in background updates. The exception is when we
|
||||
# create indices on tables we've just created.
|
||||
created_tables = set()
|
||||
for delta_file in changed_delta_files:
|
||||
with open(delta_file) as fd:
|
||||
delta_lines = fd.readlines()
|
||||
|
||||
for f in bad_files:
|
||||
click.secho(
|
||||
f"\t{f}",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
for line in delta_lines:
|
||||
# Strip SQL comments
|
||||
line = line.split("--", maxsplit=1)[0]
|
||||
|
||||
click.secho()
|
||||
click.secho(
|
||||
f"Please move these files to delta/{current_schema_version}/",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
# Check and track any tables we create
|
||||
match = TABLE_CREATION_REGEX.search(line)
|
||||
if match:
|
||||
table_name = match.group(1)
|
||||
created_tables.add(table_name)
|
||||
|
||||
click.get_current_context().exit(1)
|
||||
# Check for dropping indices, these are always banned
|
||||
match = INDEX_DELETION_REGEX.search(line)
|
||||
if match:
|
||||
clause = match.group()
|
||||
|
||||
click.secho(
|
||||
f"Found delta with index deletion: '{clause}' in {delta_file}\nThese should be in background updates.",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
return_code = 1
|
||||
|
||||
# Check for index creation, which is only allowed for tables we've
|
||||
# created.
|
||||
match = INDEX_CREATION_REGEX.search(line)
|
||||
if match:
|
||||
clause = match.group()
|
||||
table_name = match.group(1)
|
||||
if table_name not in created_tables:
|
||||
click.secho(
|
||||
f"Found delta with index creation: '{clause}' in {delta_file}\nThese should be in background updates.",
|
||||
fg="red",
|
||||
bold=True,
|
||||
color=force_colors,
|
||||
)
|
||||
return_code = 1
|
||||
|
||||
click.get_current_context().exit(return_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
503
scripts-dev/gen_config_documentation.py
Executable file
503
scripts-dev/gen_config_documentation.py
Executable file
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Synapse documentation from JSON Schema file."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
HEADER = """<!-- Document auto-generated by scripts-dev/gen_config_documentation.py -->
|
||||
|
||||
# Configuring Synapse
|
||||
|
||||
This is intended as a guide to the Synapse configuration. The behavior of a Synapse instance can be modified
|
||||
through the many configuration settings documented here — each config option is explained,
|
||||
including what the default is, how to change the default and what sort of behaviour the setting governs.
|
||||
Also included is an example configuration for each setting. If you don't want to spend a lot of time
|
||||
thinking about options, the config as generated sets sensible defaults for all values. Do note however that the
|
||||
database defaults to SQLite, which is not recommended for production usage. You can read more on this subject
|
||||
[here](../../setup/installation.md#using-postgresql).
|
||||
|
||||
## Config Conventions
|
||||
|
||||
Configuration options that take a time period can be set using a number
|
||||
followed by a letter. Letters have the following meanings:
|
||||
|
||||
* `s` = second
|
||||
* `m` = minute
|
||||
* `h` = hour
|
||||
* `d` = day
|
||||
* `w` = week
|
||||
* `y` = year
|
||||
|
||||
For example, setting `redaction_retention_period: 5m` would remove redacted
|
||||
messages from the database after 5 minutes, rather than 5 months.
|
||||
|
||||
In addition, configuration options referring to size use the following suffixes:
|
||||
|
||||
* `K` = KiB, or 1024 bytes
|
||||
* `M` = MiB, or 1,048,576 bytes
|
||||
* `G` = GiB, or 1,073,741,824 bytes
|
||||
* `T` = TiB, or 1,099,511,627,776 bytes
|
||||
|
||||
For example, setting `max_avatar_size: 10M` means that Synapse will not accept files larger than 10,485,760 bytes
|
||||
for a user avatar.
|
||||
|
||||
## Config Validation
|
||||
|
||||
The configuration file can be validated with the following command:
|
||||
```bash
|
||||
python -m synapse.config read <config key to print> -c <path to config>
|
||||
```
|
||||
|
||||
To validate the entire file, omit `read <config key to print>`:
|
||||
```bash
|
||||
python -m synapse.config -c <path to config>
|
||||
```
|
||||
|
||||
To see how to set other options, check the help reference:
|
||||
```bash
|
||||
python -m synapse.config --help
|
||||
```
|
||||
|
||||
### YAML
|
||||
The configuration file is a [YAML](https://yaml.org/) file, which means that certain syntax rules
|
||||
apply if you want your config file to be read properly. A few helpful things to know:
|
||||
* `#` before any option in the config will comment out that setting and either a default (if available) will
|
||||
be applied or Synapse will ignore the setting. Thus, in example #1 below, the setting will be read and
|
||||
applied, but in example #2 the setting will not be read and a default will be applied.
|
||||
|
||||
Example #1:
|
||||
```yaml
|
||||
pid_file: DATADIR/homeserver.pid
|
||||
```
|
||||
Example #2:
|
||||
```yaml
|
||||
#pid_file: DATADIR/homeserver.pid
|
||||
```
|
||||
* Indentation matters! The indentation before a setting
|
||||
will determine whether a given setting is read as part of another
|
||||
setting, or considered on its own. Thus, in example #1, the `enabled` setting
|
||||
is read as a sub-option of the `presence` setting, and will be properly applied.
|
||||
|
||||
However, the lack of indentation before the `enabled` setting in example #2 means
|
||||
that when reading the config, Synapse will consider both `presence` and `enabled` as
|
||||
different settings. In this case, `presence` has no value, and thus a default applied, and `enabled`
|
||||
is an option that Synapse doesn't recognize and thus ignores.
|
||||
|
||||
Example #1:
|
||||
```yaml
|
||||
presence:
|
||||
enabled: false
|
||||
```
|
||||
Example #2:
|
||||
```yaml
|
||||
presence:
|
||||
enabled: false
|
||||
```
|
||||
In this manual, all top-level settings (ones with no indentation) are identified
|
||||
at the beginning of their section (i.e. "### `example_setting`") and
|
||||
the sub-options, if any, are identified and listed in the body of the section.
|
||||
In addition, each setting has an example of its usage, with the proper indentation
|
||||
shown.
|
||||
"""
|
||||
SECTION_HEADERS = {
|
||||
"modules": {
|
||||
"title": "Modules",
|
||||
"description": (
|
||||
"Server admins can expand Synapse's functionality with external "
|
||||
"modules.\n\n"
|
||||
"See [here](../../modules/index.md) for more documentation on how "
|
||||
"to configure or create custom modules for Synapse."
|
||||
),
|
||||
},
|
||||
"server_name": {
|
||||
"title": "Server",
|
||||
"description": "Define your homeserver name and other base options.",
|
||||
},
|
||||
"admin_contact": {
|
||||
"title": "Homeserver blocking",
|
||||
"description": "Useful options for Synapse admins.",
|
||||
},
|
||||
"tls_certificate_path": {
|
||||
"title": "TLS",
|
||||
"description": "Options related to TLS.",
|
||||
},
|
||||
"federation_domain_whitelist": {
|
||||
"title": "Federation",
|
||||
"description": "Options related to federation.",
|
||||
},
|
||||
"event_cache_size": {
|
||||
"title": "Caching",
|
||||
"description": "Options related to caching.",
|
||||
},
|
||||
"database": {
|
||||
"title": "Database",
|
||||
"description": "Config options related to database settings.",
|
||||
},
|
||||
"log_config": {
|
||||
"title": "Logging",
|
||||
"description": ("Config options related to logging."),
|
||||
},
|
||||
"rc_message": {
|
||||
"title": "Ratelimiting",
|
||||
"description": (
|
||||
"Options related to ratelimiting in Synapse.\n\n"
|
||||
"Each ratelimiting configuration is made of two parameters:\n"
|
||||
"- `per_second`: number of requests a client can send per second.\n"
|
||||
"- `burst_count`: number of requests a client can send before "
|
||||
"being throttled."
|
||||
),
|
||||
},
|
||||
"enable_authenticated_media": {
|
||||
"title": "Media Store",
|
||||
"description": "Config options related to Synapse's media store.",
|
||||
},
|
||||
"recaptcha_public_key": {
|
||||
"title": "Captcha",
|
||||
"description": (
|
||||
"See [here](../../CAPTCHA_SETUP.md) for full details on setting up captcha."
|
||||
),
|
||||
},
|
||||
"turn_uris": {
|
||||
"title": "TURN",
|
||||
"description": ("Options related to adding a TURN server to Synapse."),
|
||||
},
|
||||
"enable_registration": {
|
||||
"title": "Registration",
|
||||
"description": (
|
||||
"Registration can be rate-limited using the parameters in the "
|
||||
"[Ratelimiting](#ratelimiting) section of this manual."
|
||||
),
|
||||
},
|
||||
"session_lifetime": {
|
||||
"title": "User session management",
|
||||
"description": ("Config options related to user session management."),
|
||||
},
|
||||
"enable_metrics": {
|
||||
"title": "Metrics",
|
||||
"description": ("Config options related to metrics."),
|
||||
},
|
||||
"room_prejoin_state": {
|
||||
"title": "API Configuration",
|
||||
"description": ("Config settings related to the client/server API."),
|
||||
},
|
||||
"signing_key_path": {
|
||||
"title": "Signing Keys",
|
||||
"description": ("Config options relating to signing keys."),
|
||||
},
|
||||
"saml2_config": {
|
||||
"title": "Single sign-on integration",
|
||||
"description": (
|
||||
"The following settings can be used to make Synapse use a single sign-on provider for authentication, instead of its internal password database.\n\n"
|
||||
"You will probably also want to set the following options to `false` to disable the regular login/registration flows:\n"
|
||||
"* [`enable_registration`](#enable_registration)\n"
|
||||
"* [`password_config.enabled`](#password_config)"
|
||||
),
|
||||
},
|
||||
"push": {
|
||||
"title": "Push",
|
||||
"description": ("Configuration settings related to push notifications."),
|
||||
},
|
||||
"encryption_enabled_by_default_for_room_type": {
|
||||
"title": "Rooms",
|
||||
"description": ("Config options relating to rooms."),
|
||||
},
|
||||
"opentracing": {
|
||||
"title": "Opentracing",
|
||||
"description": ("Configuration options related to Opentracing support."),
|
||||
},
|
||||
"worker_replication_secret": {
|
||||
"title": "Coordinating workers",
|
||||
"description": (
|
||||
"Configuration options related to workers which belong in the main config file (usually called `homeserver.yaml`). A Synapse deployment can scale horizontally by running multiple Synapse processes called _workers_. Incoming requests are distributed between workers to handle higher loads. Some workers are privileged and can accept requests from other workers.\n\n"
|
||||
"As a result, the worker configuration is divided into two parts.\n\n"
|
||||
"1. The first part (in this section of the manual) defines which shardable tasks are delegated to privileged workers. This allows unprivileged workers to make requests to a privileged worker to act on their behalf.\n"
|
||||
"2. [The second part](#individual-worker-configuration) controls the behaviour of individual workers in isolation.\n\n"
|
||||
"For guidance on setting up workers, see the [worker documentation](../../workers.md)."
|
||||
),
|
||||
},
|
||||
"worker_app": {
|
||||
"title": "Individual worker configuration",
|
||||
"description": (
|
||||
"These options configure an individual worker, in its worker configuration file. They should be not be provided when configuring the main process.\n\n"
|
||||
"Note also the configuration above for [coordinating a cluster of workers](#coordinating-workers).\n\n"
|
||||
"For guidance on setting up workers, see the [worker documentation](../../workers.md)."
|
||||
),
|
||||
},
|
||||
"background_updates": {
|
||||
"title": "Background Updates",
|
||||
"description": ("Configuration settings related to background updates."),
|
||||
},
|
||||
"auto_accept_invites": {
|
||||
"title": "Auto Accept Invites",
|
||||
"description": (
|
||||
"Configuration settings related to automatically accepting invites."
|
||||
),
|
||||
},
|
||||
}
|
||||
INDENT = " "
|
||||
|
||||
|
||||
has_error = False
|
||||
|
||||
|
||||
def error(text: str) -> None:
|
||||
global has_error
|
||||
print(f"ERROR: {text}", file=sys.stderr)
|
||||
has_error = True
|
||||
|
||||
|
||||
def indent(text: str, first_line: bool = True) -> str:
|
||||
"""Indents each non-empty line of the given text."""
|
||||
text = re.sub(r"(\n)([^\n])", r"\1" + INDENT + r"\2", text)
|
||||
if first_line:
|
||||
text = re.sub(r"^([^\n])", INDENT + r"\1", text)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def em(s: Optional[str]) -> str:
|
||||
"""Add emphasis to text."""
|
||||
return f"*{s}*" if s else ""
|
||||
|
||||
|
||||
def a(s: Optional[str], suffix: str = " ") -> str:
|
||||
"""Appends a space if the given string is not empty."""
|
||||
return s + suffix if s else ""
|
||||
|
||||
|
||||
def p(s: Optional[str], prefix: str = " ") -> str:
|
||||
"""Prepend a space if the given string is not empty."""
|
||||
return prefix + s if s else ""
|
||||
|
||||
|
||||
def resolve_local_refs(schema: dict) -> dict:
|
||||
"""Returns the given schema with local $ref properties replaced by their keywords.
|
||||
|
||||
Crude approximation that will override keywords.
|
||||
"""
|
||||
defs = schema["$defs"]
|
||||
|
||||
def replace_ref(d: Any) -> Any:
|
||||
if isinstance(d, dict):
|
||||
the_def = {}
|
||||
if "$ref" in d:
|
||||
# Found a "$ref" key.
|
||||
def_name = d["$ref"].removeprefix("#/$defs/")
|
||||
del d["$ref"]
|
||||
the_def = defs[def_name]
|
||||
|
||||
new_dict = {k: replace_ref(v) for k, v in d.items()}
|
||||
if common_keys := (new_dict.keys() & the_def.keys()) - {"properties"}:
|
||||
print(
|
||||
f"WARN: '{def_name}' overrides keys '{common_keys}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
new_dict_props = new_dict.get("properties", {})
|
||||
the_def_props = the_def.get("properties", {})
|
||||
if common_props := new_dict_props.keys() & the_def_props.keys():
|
||||
print(
|
||||
f"WARN: '{def_name}' overrides properties '{common_props}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if merged_props := {**new_dict_props, **the_def_props}:
|
||||
return {**new_dict, **the_def, "properties": merged_props}
|
||||
else:
|
||||
return {**new_dict, **the_def}
|
||||
|
||||
elif isinstance(d, list):
|
||||
return [replace_ref(v) for v in d]
|
||||
else:
|
||||
return d
|
||||
|
||||
return replace_ref(schema)
|
||||
|
||||
|
||||
def sep(values: dict) -> str:
|
||||
"""Separator between parts of the description."""
|
||||
# If description is multiple paragraphs already, add new ones. Otherwise
|
||||
# append to same paragraph.
|
||||
return "\n\n" if "\n\n" in values.get("description", "") else " "
|
||||
|
||||
|
||||
def type_str(values: dict) -> str:
|
||||
"""Type of the current value."""
|
||||
if t := values.get("io.element.type_name"):
|
||||
# Allow custom overrides for the type name, for documentation clarity
|
||||
return f"({t})"
|
||||
if not (t := values.get("type")):
|
||||
return ""
|
||||
if not isinstance(t, list):
|
||||
t = [t]
|
||||
joined = "|".join(t)
|
||||
return f"({joined})"
|
||||
|
||||
|
||||
def items(values: dict) -> str:
|
||||
"""A block listing properties of array items."""
|
||||
if not (items := values.get("items")):
|
||||
return ""
|
||||
if not (item_props := items.get("properties")):
|
||||
return ""
|
||||
return "\nOptions for each entry include:\n\n" + "\n".join(
|
||||
sub_section(k, v) for k, v in item_props.items()
|
||||
)
|
||||
|
||||
|
||||
def properties(values: dict) -> str:
|
||||
"""A block listing object properties."""
|
||||
if not (properties := values.get("properties")):
|
||||
return ""
|
||||
return "\nThis setting has the following sub-options:\n\n" + "\n".join(
|
||||
sub_section(k, v) for k, v in properties.items()
|
||||
)
|
||||
|
||||
|
||||
def sub_section(prop: str, values: dict) -> str:
|
||||
"""Formats a bullet point about the given sub-property."""
|
||||
sep = lambda: globals()["sep"](values)
|
||||
type_str = lambda: globals()["type_str"](values)
|
||||
items = lambda: globals()["items"](values)
|
||||
properties = lambda: globals()["properties"](values)
|
||||
|
||||
def default() -> str:
|
||||
try:
|
||||
default = values["default"]
|
||||
return f"Defaults to `{json.dumps(default)}`."
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
def description() -> str:
|
||||
if not (description := values.get("description")):
|
||||
error(f"missing description for {prop}")
|
||||
return "MISSING DESCRIPTION\n"
|
||||
|
||||
return f"{description}{p(default(), sep())}\n"
|
||||
|
||||
return (
|
||||
f"* `{prop}`{p(type_str())}: "
|
||||
+ f"{indent(description(), first_line=False)}"
|
||||
+ indent(items())
|
||||
+ indent(properties())
|
||||
)
|
||||
|
||||
|
||||
def section(prop: str, values: dict) -> str:
|
||||
"""Formats a section about the given property."""
|
||||
sep = lambda: globals()["sep"](values)
|
||||
type_str = lambda: globals()["type_str"](values)
|
||||
items = lambda: globals()["items"](values)
|
||||
properties = lambda: globals()["properties"](values)
|
||||
|
||||
def is_simple_default() -> bool:
|
||||
"""Whether the given default is simple enough for a one-liner."""
|
||||
if not (d := values.get("default")):
|
||||
return True
|
||||
return not isinstance(d, dict) and not isinstance(d, list)
|
||||
|
||||
def default_str() -> str:
|
||||
try:
|
||||
default = values["default"]
|
||||
except KeyError:
|
||||
t = values.get("type", [])
|
||||
if "object" == t or "object" in t:
|
||||
# Skip objects as they probably have child defaults.
|
||||
return ""
|
||||
return "There is no default for this option."
|
||||
|
||||
if not is_simple_default():
|
||||
# Show complex defaults as a code block instead.
|
||||
return ""
|
||||
return f"Defaults to `{json.dumps(default)}`."
|
||||
|
||||
def header() -> str:
|
||||
try:
|
||||
title = SECTION_HEADERS[prop]["title"]
|
||||
description = SECTION_HEADERS[prop]["description"]
|
||||
return f"## {title}\n\n{description}\n\n---\n"
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
def title() -> str:
|
||||
return f"### `{prop}`\n"
|
||||
|
||||
def description() -> str:
|
||||
if not (description := values.get("description")):
|
||||
error(f"missing description for {prop}")
|
||||
return "MISSING DESCRIPTION\n"
|
||||
return f"\n{a(em(type_str()))}{description}{p(default_str(), sep())}\n"
|
||||
|
||||
def example_str(example: Any) -> str:
|
||||
return "```yaml\n" + f"{yaml.dump({prop: example}, sort_keys=False)}" + "```\n"
|
||||
|
||||
def default_example() -> str:
|
||||
if is_simple_default():
|
||||
return ""
|
||||
default_cfg = example_str(values["default"])
|
||||
return f"\nDefault configuration:\n{default_cfg}"
|
||||
|
||||
def examples() -> str:
|
||||
if not (examples := values.get("examples")):
|
||||
return ""
|
||||
|
||||
examples_str = "\n".join(example_str(e) for e in examples)
|
||||
|
||||
if len(examples) >= 2:
|
||||
return f"\nExample configurations:\n{examples_str}"
|
||||
else:
|
||||
return f"\nExample configuration:\n{examples_str}"
|
||||
|
||||
def post_description() -> str:
|
||||
# Sometimes it's helpful to have a description after the list of fields,
|
||||
# e.g. with a subsection that consists only of text.
|
||||
# This helps with that.
|
||||
if not (description := values.get("io.element.post_description")):
|
||||
return ""
|
||||
return f"\n{description}\n\n"
|
||||
|
||||
return (
|
||||
"---\n"
|
||||
+ header()
|
||||
+ title()
|
||||
+ description()
|
||||
+ items()
|
||||
+ properties()
|
||||
+ default_example()
|
||||
+ examples()
|
||||
+ post_description()
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def usage(err_msg: str) -> int:
|
||||
script_name = (sys.argv[:1] or ["__main__.py"])[0]
|
||||
print(err_msg, file=sys.stderr)
|
||||
print(f"Usage: {script_name} <JSON Schema file>", file=sys.stderr)
|
||||
print(f"\n{__doc__}", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
def read_json_file_arg() -> Any:
|
||||
if len(sys.argv) > 2:
|
||||
exit(usage("Too many arguments."))
|
||||
if not (filepath := (sys.argv[1:] or [""])[0]):
|
||||
exit(usage("No schema file provided."))
|
||||
with open(filepath) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
schema = read_json_file_arg()
|
||||
schema = resolve_local_refs(schema)
|
||||
|
||||
sections = (section(k, v) for k, v in schema["properties"].items())
|
||||
print(HEADER + "".join(sections), end="")
|
||||
|
||||
if has_error:
|
||||
print("There were errors.", file=sys.stderr)
|
||||
exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -254,6 +254,12 @@ def _prepare() -> None:
|
||||
# Update the version specified in pyproject.toml.
|
||||
subprocess.check_output(["poetry", "version", new_version])
|
||||
|
||||
# Update config schema $id.
|
||||
schema_file = "schema/synapse-config.schema.yaml"
|
||||
major_minor_version = ".".join(new_version.split(".")[:2])
|
||||
url = f"https://element-hq.github.io/synapse/schema/synapse/v{major_minor_version}/synapse-config.schema.json"
|
||||
subprocess.check_output(["sed", "-i", f"0,/^\\$id: .*/s||$id: {url}|", schema_file])
|
||||
|
||||
# Generate changelogs.
|
||||
generate_and_write_changelog(synapse_repo, current_version, new_version)
|
||||
|
||||
|
||||
@@ -1065,7 +1065,7 @@ class Porter:
|
||||
|
||||
def get_sent_table_size(txn: LoggingTransaction) -> int:
|
||||
txn.execute(
|
||||
"SELECT count(*) FROM sent_transactions" " WHERE ts >= ?", (yesterday,)
|
||||
"SELECT count(*) FROM sent_transactions WHERE ts >= ?", (yesterday,)
|
||||
)
|
||||
result = txn.fetchone()
|
||||
assert result is not None
|
||||
|
||||
@@ -292,9 +292,9 @@ def main() -> None:
|
||||
for key in worker_config:
|
||||
if key == "worker_app": # But we allow worker_app
|
||||
continue
|
||||
assert not key.startswith(
|
||||
"worker_"
|
||||
), "Main process cannot use worker_* config"
|
||||
assert not key.startswith("worker_"), (
|
||||
"Main process cannot use worker_* config"
|
||||
)
|
||||
else:
|
||||
worker_pidfile = worker_config["worker_pid_file"]
|
||||
worker_cache_factor = worker_config.get("synctl_cache_factor")
|
||||
|
||||
@@ -39,7 +39,6 @@ from synapse.api.errors import (
|
||||
HttpResponseException,
|
||||
InvalidClientTokenError,
|
||||
OAuthInsufficientScopeError,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
UnrecognizedRequestError,
|
||||
)
|
||||
@@ -512,7 +511,7 @@ class MSC3861DelegatedAuth(BaseAuth):
|
||||
raise InvalidClientTokenError("No scope in token granting user rights")
|
||||
|
||||
# Match via the sub claim
|
||||
sub: Optional[str] = introspection_result.get_sub()
|
||||
sub = introspection_result.get_sub()
|
||||
if sub is None:
|
||||
raise InvalidClientTokenError(
|
||||
"Invalid sub claim in the introspection result"
|
||||
@@ -525,29 +524,20 @@ class MSC3861DelegatedAuth(BaseAuth):
|
||||
# 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):
|
||||
username = introspection_result.get_username()
|
||||
if username is None:
|
||||
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
|
||||
# Try to find a user from the username claim
|
||||
user_info = await self.store.get_user_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
|
||||
raise AuthError(
|
||||
500,
|
||||
"User not found",
|
||||
)
|
||||
|
||||
# And record the sub as external_id
|
||||
@@ -587,17 +577,10 @@ class MSC3861DelegatedAuth(BaseAuth):
|
||||
"Invalid device ID in introspection result",
|
||||
)
|
||||
|
||||
# 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",
|
||||
)
|
||||
# Make sure the device exists
|
||||
await self.store.get_device(
|
||||
user_id=user_id.to_string(), device_id=device_id
|
||||
)
|
||||
|
||||
# TODO: there is a few things missing in the requester here, which still need
|
||||
# to be figured out, like:
|
||||
|
||||
@@ -70,6 +70,7 @@ class Codes(str, Enum):
|
||||
THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
|
||||
THREEPID_DENIED = "M_THREEPID_DENIED"
|
||||
INVALID_USERNAME = "M_INVALID_USERNAME"
|
||||
THREEPID_MEDIUM_NOT_SUPPORTED = "M_THREEPID_MEDIUM_NOT_SUPPORTED"
|
||||
SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
|
||||
CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||
CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Hashable, Optional, Tuple
|
||||
from typing import Dict, Hashable, Optional, Tuple
|
||||
|
||||
from synapse.api.errors import LimitExceededError
|
||||
from synapse.config.ratelimiting import RatelimitSettings
|
||||
@@ -80,12 +79,14 @@ class Ratelimiter:
|
||||
self.store = store
|
||||
self._limiter_name = cfg.key
|
||||
|
||||
# An ordered dictionary representing the token buckets tracked by this rate
|
||||
# A dictionary representing the token buckets tracked by this rate
|
||||
# limiter. Each entry maps a key of arbitrary type to a tuple representing:
|
||||
# * The number of tokens currently in the bucket,
|
||||
# * The time point when the bucket was last completely empty, and
|
||||
# * The rate_hz (leak rate) of this particular bucket.
|
||||
self.actions: OrderedDict[Hashable, Tuple[float, float, float]] = OrderedDict()
|
||||
self.actions: Dict[Hashable, Tuple[float, float, float]] = {}
|
||||
|
||||
self.clock.looping_call(self._prune_message_counts, 60 * 1000)
|
||||
|
||||
def _get_key(
|
||||
self, requester: Optional[Requester], key: Optional[Hashable]
|
||||
@@ -169,9 +170,6 @@ class Ratelimiter:
|
||||
rate_hz = rate_hz if rate_hz is not None else self.rate_hz
|
||||
burst_count = burst_count if burst_count is not None else self.burst_count
|
||||
|
||||
# Remove any expired entries
|
||||
self._prune_message_counts(time_now_s)
|
||||
|
||||
# Check if there is an existing count entry for this key
|
||||
action_count, time_start, _ = self._get_action_counts(key, time_now_s)
|
||||
|
||||
@@ -246,13 +244,12 @@ class Ratelimiter:
|
||||
action_count, time_start, rate_hz = self._get_action_counts(key, time_now_s)
|
||||
self.actions[key] = (action_count + n_actions, time_start, rate_hz)
|
||||
|
||||
def _prune_message_counts(self, time_now_s: float) -> None:
|
||||
def _prune_message_counts(self) -> None:
|
||||
"""Remove message count entries that have not exceeded their defined
|
||||
rate_hz limit
|
||||
|
||||
Args:
|
||||
time_now_s: The current time
|
||||
"""
|
||||
time_now_s = self.clock.time()
|
||||
|
||||
# We create a copy of the key list here as the dictionary is modified during
|
||||
# the loop
|
||||
for key in list(self.actions.keys()):
|
||||
|
||||
@@ -51,8 +51,7 @@ from synapse.http.server import JsonResource, OptionsResource
|
||||
from synapse.logging.context import LoggingContext
|
||||
from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy
|
||||
from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
|
||||
from synapse.rest import ClientRestResource
|
||||
from synapse.rest.admin import register_servlets_for_media_repo
|
||||
from synapse.rest import ClientRestResource, admin
|
||||
from synapse.rest.health import HealthResource
|
||||
from synapse.rest.key.v2 import KeyResource
|
||||
from synapse.rest.synapse.client import build_synapse_client_resource_tree
|
||||
@@ -176,8 +175,13 @@ class GenericWorkerServer(HomeServer):
|
||||
def _listen_http(self, listener_config: ListenerConfig) -> None:
|
||||
assert listener_config.http_options is not None
|
||||
|
||||
# We always include a health resource.
|
||||
resources: Dict[str, Resource] = {"/health": HealthResource()}
|
||||
# We always include an admin resource that we populate with servlets as needed
|
||||
admin_resource = JsonResource(self, canonical_json=False)
|
||||
resources: Dict[str, Resource] = {
|
||||
# We always include a health resource.
|
||||
"/health": HealthResource(),
|
||||
"/_synapse/admin": admin_resource,
|
||||
}
|
||||
|
||||
for res in listener_config.http_options.resources:
|
||||
for name in res.names:
|
||||
@@ -190,6 +194,7 @@ class GenericWorkerServer(HomeServer):
|
||||
|
||||
resources.update(build_synapse_client_resource_tree(self))
|
||||
resources["/.well-known"] = well_known_resource(self)
|
||||
admin.register_servlets(self, admin_resource)
|
||||
|
||||
elif name == "federation":
|
||||
resources[FEDERATION_PREFIX] = TransportLayerServer(self)
|
||||
@@ -199,15 +204,13 @@ class GenericWorkerServer(HomeServer):
|
||||
|
||||
# We need to serve the admin servlets for media on the
|
||||
# worker.
|
||||
admin_resource = JsonResource(self, canonical_json=False)
|
||||
register_servlets_for_media_repo(self, admin_resource)
|
||||
admin.register_servlets_for_media_repo(self, admin_resource)
|
||||
|
||||
resources.update(
|
||||
{
|
||||
MEDIA_R0_PREFIX: media_repo,
|
||||
MEDIA_V3_PREFIX: media_repo,
|
||||
LEGACY_MEDIA_PREFIX: media_repo,
|
||||
"/_synapse/admin": admin_resource,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -284,8 +287,7 @@ class GenericWorkerServer(HomeServer):
|
||||
elif listener.type == "metrics":
|
||||
if not self.config.metrics.enable_metrics:
|
||||
logger.warning(
|
||||
"Metrics listener configured, but "
|
||||
"enable_metrics is not True!"
|
||||
"Metrics listener configured, but enable_metrics is not True!"
|
||||
)
|
||||
else:
|
||||
if isinstance(listener, TCPListenerConfig):
|
||||
|
||||
@@ -54,6 +54,7 @@ from synapse.config.server import ListenerConfig, TCPListenerConfig
|
||||
from synapse.federation.transport.server import TransportLayerServer
|
||||
from synapse.http.additional_resource import AdditionalResource
|
||||
from synapse.http.server import (
|
||||
JsonResource,
|
||||
OptionsResource,
|
||||
RootOptionsRedirectResource,
|
||||
StaticResource,
|
||||
@@ -61,8 +62,7 @@ from synapse.http.server import (
|
||||
from synapse.logging.context import LoggingContext
|
||||
from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy
|
||||
from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
|
||||
from synapse.rest import ClientRestResource
|
||||
from synapse.rest.admin import AdminRestResource
|
||||
from synapse.rest import ClientRestResource, admin
|
||||
from synapse.rest.health import HealthResource
|
||||
from synapse.rest.key.v2 import KeyResource
|
||||
from synapse.rest.synapse.client import build_synapse_client_resource_tree
|
||||
@@ -180,11 +180,14 @@ class SynapseHomeServer(HomeServer):
|
||||
if compress:
|
||||
client_resource = gz_wrap(client_resource)
|
||||
|
||||
admin_resource = JsonResource(self, canonical_json=False)
|
||||
admin.register_servlets(self, admin_resource)
|
||||
|
||||
resources.update(
|
||||
{
|
||||
CLIENT_API_PREFIX: client_resource,
|
||||
"/.well-known": well_known_resource(self),
|
||||
"/_synapse/admin": AdminRestResource(self),
|
||||
"/_synapse/admin": admin_resource,
|
||||
**build_synapse_client_resource_tree(self),
|
||||
}
|
||||
)
|
||||
@@ -286,8 +289,7 @@ class SynapseHomeServer(HomeServer):
|
||||
elif listener.type == "metrics":
|
||||
if not self.config.metrics.enable_metrics:
|
||||
logger.warning(
|
||||
"Metrics listener configured, but "
|
||||
"enable_metrics is not True!"
|
||||
"Metrics listener configured, but enable_metrics is not True!"
|
||||
)
|
||||
else:
|
||||
if isinstance(listener, TCPListenerConfig):
|
||||
|
||||
@@ -34,6 +34,22 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger("synapse.app.homeserver")
|
||||
|
||||
ONE_MINUTE_SECONDS = 60
|
||||
ONE_HOUR_SECONDS = 60 * ONE_MINUTE_SECONDS
|
||||
|
||||
MILLISECONDS_PER_SECOND = 1000
|
||||
|
||||
INITIAL_DELAY_BEFORE_FIRST_PHONE_HOME_SECONDS = 5 * ONE_MINUTE_SECONDS
|
||||
"""
|
||||
We wait 5 minutes to send the first set of stats as the server can be quite busy the
|
||||
first few minutes
|
||||
"""
|
||||
|
||||
PHONE_HOME_INTERVAL_SECONDS = 3 * ONE_HOUR_SECONDS
|
||||
"""
|
||||
Phone home stats are sent every 3 hours
|
||||
"""
|
||||
|
||||
# Contains the list of processes we will be monitoring
|
||||
# currently either 0 or 1
|
||||
_stats_process: List[Tuple[int, "resource.struct_rusage"]] = []
|
||||
@@ -185,12 +201,14 @@ def start_phone_stats_home(hs: "HomeServer") -> None:
|
||||
# If you increase the loop period, the accuracy of user_daily_visits
|
||||
# table will decrease
|
||||
clock.looping_call(
|
||||
hs.get_datastores().main.generate_user_daily_visits, 5 * 60 * 1000
|
||||
hs.get_datastores().main.generate_user_daily_visits,
|
||||
5 * ONE_MINUTE_SECONDS * MILLISECONDS_PER_SECOND,
|
||||
)
|
||||
|
||||
# monthly active user limiting functionality
|
||||
clock.looping_call(
|
||||
hs.get_datastores().main.reap_monthly_active_users, 1000 * 60 * 60
|
||||
hs.get_datastores().main.reap_monthly_active_users,
|
||||
ONE_HOUR_SECONDS * MILLISECONDS_PER_SECOND,
|
||||
)
|
||||
hs.get_datastores().main.reap_monthly_active_users()
|
||||
|
||||
@@ -221,7 +239,12 @@ def start_phone_stats_home(hs: "HomeServer") -> None:
|
||||
|
||||
if hs.config.metrics.report_stats:
|
||||
logger.info("Scheduling stats reporting for 3 hour intervals")
|
||||
clock.looping_call(phone_stats_home, 3 * 60 * 60 * 1000, hs, stats)
|
||||
clock.looping_call(
|
||||
phone_stats_home,
|
||||
PHONE_HOME_INTERVAL_SECONDS * MILLISECONDS_PER_SECOND,
|
||||
hs,
|
||||
stats,
|
||||
)
|
||||
|
||||
# We need to defer this init for the cases that we daemonize
|
||||
# otherwise the process ID we get is that of the non-daemon process
|
||||
@@ -229,4 +252,6 @@ def start_phone_stats_home(hs: "HomeServer") -> None:
|
||||
|
||||
# We wait 5 minutes to send the first set of stats as the server can
|
||||
# be quite busy the first few minutes
|
||||
clock.call_later(5 * 60, phone_stats_home, hs, stats)
|
||||
clock.call_later(
|
||||
INITIAL_DELAY_BEFORE_FIRST_PHONE_HOME_SECONDS, phone_stats_home, hs, stats
|
||||
)
|
||||
|
||||
@@ -170,7 +170,7 @@ class Config:
|
||||
|
||||
section: ClassVar[str]
|
||||
|
||||
def __init__(self, root_config: "RootConfig" = None):
|
||||
def __init__(self, root_config: "RootConfig"):
|
||||
self.root = root_config
|
||||
|
||||
# Get the path to the default Synapse template directory
|
||||
@@ -445,7 +445,7 @@ class RootConfig:
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def invoke_all_static(cls, func_name: str, *args: Any, **kwargs: any) -> None:
|
||||
def invoke_all_static(cls, func_name: str, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Invoke a static function on config objects this RootConfig is
|
||||
configured to use.
|
||||
@@ -1047,7 +1047,7 @@ class RoutableShardedWorkerHandlingConfig(ShardedWorkerHandlingConfig):
|
||||
return self._get_instance(key)
|
||||
|
||||
|
||||
def read_file(file_path: Any, config_path: Iterable[str]) -> str:
|
||||
def read_file(file_path: Any, config_path: StrSequence) -> str:
|
||||
"""Check the given file exists, and read it into a string
|
||||
|
||||
If it does not, emit an error indicating the problem
|
||||
|
||||
@@ -179,7 +179,7 @@ class RootConfig:
|
||||
class Config:
|
||||
root: RootConfig
|
||||
default_template_dir: str
|
||||
def __init__(self, root_config: Optional[RootConfig] = ...) -> None: ...
|
||||
def __init__(self, root_config: RootConfig = ...) -> None: ...
|
||||
@staticmethod
|
||||
def parse_size(value: Union[str, int]) -> int: ...
|
||||
@staticmethod
|
||||
@@ -212,4 +212,4 @@ class ShardedWorkerHandlingConfig:
|
||||
class RoutableShardedWorkerHandlingConfig(ShardedWorkerHandlingConfig):
|
||||
def get_instance(self, key: str) -> str: ... # noqa: F811
|
||||
|
||||
def read_file(file_path: Any, config_path: Iterable[str]) -> str: ...
|
||||
def read_file(file_path: Any, config_path: StrSequence) -> str: ...
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import enum
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING, Any, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
import attr
|
||||
import attr.validators
|
||||
@@ -29,7 +29,7 @@ import attr.validators
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
|
||||
from synapse.config import ConfigError
|
||||
from synapse.config._base import Config, RootConfig, read_file
|
||||
from synapse.types import JsonDict
|
||||
from synapse.types import JsonDict, StrSequence
|
||||
|
||||
# Determine whether authlib is installed.
|
||||
try:
|
||||
@@ -45,7 +45,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
@cache
|
||||
def read_secret_from_file_once(file_path: Any, config_path: Iterable[str]) -> str:
|
||||
def read_secret_from_file_once(file_path: Any, config_path: StrSequence) -> str:
|
||||
"""Returns the memoized secret read from file."""
|
||||
return read_file(file_path, config_path).strip()
|
||||
|
||||
@@ -560,3 +560,9 @@ class ExperimentalConfig(Config):
|
||||
|
||||
# MSC4076: Add `disable_badge_count`` to pusher configuration
|
||||
self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False)
|
||||
|
||||
# MSC4263: Preventing MXID enumeration via key queries
|
||||
self.msc4263_limit_key_queries_to_users_who_share_rooms = experimental.get(
|
||||
"msc4263_limit_key_queries_to_users_who_share_rooms",
|
||||
False,
|
||||
)
|
||||
|
||||
@@ -191,7 +191,7 @@ class KeyConfig(Config):
|
||||
if macaroon_secret_key:
|
||||
raise ConfigError(CONFLICTING_MACAROON_SECRET_KEY_OPTS_ERROR)
|
||||
macaroon_secret_key = read_file(
|
||||
macaroon_secret_key_path, "macaroon_secret_key_path"
|
||||
macaroon_secret_key_path, ("macaroon_secret_key_path",)
|
||||
).strip()
|
||||
if not macaroon_secret_key:
|
||||
macaroon_secret_key = self.root.registration.registration_shared_secret
|
||||
@@ -216,7 +216,9 @@ class KeyConfig(Config):
|
||||
if form_secret_path:
|
||||
if form_secret:
|
||||
raise ConfigError(CONFLICTING_FORM_SECRET_OPTS_ERROR)
|
||||
self.form_secret = read_file(form_secret_path, "form_secret_path").strip()
|
||||
self.form_secret = read_file(
|
||||
form_secret_path, ("form_secret_path",)
|
||||
).strip()
|
||||
else:
|
||||
self.form_secret = form_secret
|
||||
|
||||
|
||||
@@ -162,6 +162,10 @@ class RegistrationConfig(Config):
|
||||
"disable_msisdn_registration", False
|
||||
)
|
||||
|
||||
self.allow_underscore_prefixed_localpart = config.get(
|
||||
"allow_underscore_prefixed_localpart", False
|
||||
)
|
||||
|
||||
session_lifetime = config.get("session_lifetime")
|
||||
if session_lifetime is not None:
|
||||
session_lifetime = self.parse_duration(session_lifetime)
|
||||
|
||||
@@ -43,8 +43,7 @@ class SsoAttributeRequirement:
|
||||
"""Object describing a single requirement for SSO attributes."""
|
||||
|
||||
attribute: str
|
||||
# If neither value nor one_of is given, the attribute must simply exist. This is
|
||||
# only true for CAS configs which use a different JSON schema than the one below.
|
||||
# If neither `value` nor `one_of` is given, the attribute must simply exist.
|
||||
value: Optional[str] = None
|
||||
one_of: Optional[List[str]] = None
|
||||
|
||||
@@ -56,10 +55,6 @@ class SsoAttributeRequirement:
|
||||
"one_of": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["attribute"],
|
||||
"oneOf": [
|
||||
{"required": ["value"]},
|
||||
{"required": ["one_of"]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -108,8 +108,7 @@ class TlsConfig(Config):
|
||||
# Raise an error if this option has been specified without any
|
||||
# corresponding certificates.
|
||||
raise ConfigError(
|
||||
"federation_custom_ca_list specified without "
|
||||
"any certificate files"
|
||||
"federation_custom_ca_list specified without any certificate files"
|
||||
)
|
||||
|
||||
certs = []
|
||||
|
||||
@@ -38,6 +38,9 @@ class UserDirectoryConfig(Config):
|
||||
self.user_directory_search_all_users = user_directory_config.get(
|
||||
"search_all_users", False
|
||||
)
|
||||
self.user_directory_exclude_remote_users = user_directory_config.get(
|
||||
"exclude_remote_users", False
|
||||
)
|
||||
self.user_directory_search_prefer_local_users = user_directory_config.get(
|
||||
"prefer_local_users", False
|
||||
)
|
||||
|
||||
@@ -263,7 +263,7 @@ class WorkerConfig(Config):
|
||||
if worker_replication_secret:
|
||||
raise ConfigError(CONFLICTING_WORKER_REPLICATION_SECRET_OPTS_ERROR)
|
||||
self.worker_replication_secret = read_file(
|
||||
worker_replication_secret_path, "worker_replication_secret_path"
|
||||
worker_replication_secret_path, ("worker_replication_secret_path",)
|
||||
).strip()
|
||||
else:
|
||||
self.worker_replication_secret = worker_replication_secret
|
||||
|
||||
@@ -986,8 +986,7 @@ def _check_power_levels(
|
||||
if old_level == user_level:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You don't have permission to remove ops level equal "
|
||||
"to your own",
|
||||
"You don't have permission to remove ops level equal to your own",
|
||||
)
|
||||
|
||||
# Check if the old and new levels are greater than the user level
|
||||
|
||||
@@ -30,6 +30,7 @@ from synapse.crypto.keyring import Keyring
|
||||
from synapse.events import EventBase, make_event_from_dict
|
||||
from synapse.events.utils import prune_event, validate_canonicaljson
|
||||
from synapse.federation.units import filter_pdus_for_valid_depth
|
||||
from synapse.handlers.room_policy import RoomPolicyHandler
|
||||
from synapse.http.servlet import assert_params_in_dict
|
||||
from synapse.logging.opentracing import log_kv, trace
|
||||
from synapse.types import JsonDict, get_domain_from_id
|
||||
@@ -64,6 +65,24 @@ class FederationBase:
|
||||
self._clock = hs.get_clock()
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
|
||||
# We need to define this lazily otherwise we get a cyclic dependency.
|
||||
# self._policy_handler = hs.get_room_policy_handler()
|
||||
self._policy_handler: Optional[RoomPolicyHandler] = None
|
||||
|
||||
def _lazily_get_policy_handler(self) -> RoomPolicyHandler:
|
||||
"""Lazily get the room policy handler.
|
||||
|
||||
This is required to avoid an import cycle: RoomPolicyHandler requires a
|
||||
FederationClient, which requires a FederationBase, which requires a
|
||||
RoomPolicyHandler.
|
||||
|
||||
Returns:
|
||||
RoomPolicyHandler: The room policy handler.
|
||||
"""
|
||||
if self._policy_handler is None:
|
||||
self._policy_handler = self.hs.get_room_policy_handler()
|
||||
return self._policy_handler
|
||||
|
||||
@trace
|
||||
async def _check_sigs_and_hash(
|
||||
self,
|
||||
@@ -80,6 +99,10 @@ class FederationBase:
|
||||
Also runs the event through the spam checker; if it fails, redacts the event
|
||||
and flags it as soft-failed.
|
||||
|
||||
Also checks that the event is allowed by the policy server, if the room uses
|
||||
a policy server. If the event is not allowed, the event is flagged as
|
||||
soft-failed but not redacted.
|
||||
|
||||
Args:
|
||||
room_version: The room version of the PDU
|
||||
pdu: the event to be checked
|
||||
@@ -145,6 +168,17 @@ class FederationBase:
|
||||
)
|
||||
return redacted_event
|
||||
|
||||
policy_allowed = await self._lazily_get_policy_handler().is_event_allowed(pdu)
|
||||
if not policy_allowed:
|
||||
logger.warning(
|
||||
"Event not allowed by policy server, soft-failing %s", pdu.event_id
|
||||
)
|
||||
pdu.internal_metadata.soft_failed = True
|
||||
# Note: we don't redact the event so admins can inspect the event after the
|
||||
# fact. Other processes may redact the event, but that won't be applied to
|
||||
# the database copy of the event until the server's config requires it.
|
||||
return pdu
|
||||
|
||||
spam_check = await self._spam_checker_module_callbacks.check_event_for_spam(pdu)
|
||||
|
||||
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
|
||||
|
||||
@@ -75,6 +75,7 @@ from synapse.http.client import is_unknown_endpoint
|
||||
from synapse.http.types import QueryParams
|
||||
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace
|
||||
from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id
|
||||
from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM
|
||||
from synapse.util.async_helpers import concurrently_execute
|
||||
from synapse.util.caches.expiringcache import ExpiringCache
|
||||
from synapse.util.retryutils import NotRetryingDestination
|
||||
@@ -421,6 +422,62 @@ class FederationClient(FederationBase):
|
||||
|
||||
return None
|
||||
|
||||
@trace
|
||||
@tag_args
|
||||
async def get_pdu_policy_recommendation(
|
||||
self, destination: str, pdu: EventBase, timeout: Optional[int] = None
|
||||
) -> str:
|
||||
"""Requests that the destination server (typically a policy server)
|
||||
check the event and return its recommendation on how to handle the
|
||||
event.
|
||||
|
||||
If the policy server could not be contacted or the policy server
|
||||
returned an unknown recommendation, this returns an OK recommendation.
|
||||
This type fixing behaviour is done because the typical caller will be
|
||||
in a critical call path and would generally interpret a `None` or similar
|
||||
response as "weird value; don't care; move on without taking action". We
|
||||
just frontload that logic here.
|
||||
|
||||
|
||||
Args:
|
||||
destination: The remote homeserver to ask (a policy server)
|
||||
pdu: The event to check
|
||||
timeout: How long to try (in ms) the destination for before
|
||||
giving up. None indicates no timeout.
|
||||
|
||||
Returns:
|
||||
The policy recommendation, or RECOMMENDATION_OK if the policy server was
|
||||
uncontactable or returned an unknown recommendation.
|
||||
"""
|
||||
|
||||
logger.debug(
|
||||
"get_pdu_policy_recommendation for event_id=%s from %s",
|
||||
pdu.event_id,
|
||||
destination,
|
||||
)
|
||||
|
||||
try:
|
||||
res = await self.transport_layer.get_policy_recommendation_for_pdu(
|
||||
destination, pdu, timeout=timeout
|
||||
)
|
||||
recommendation = res.get("recommendation")
|
||||
if not isinstance(recommendation, str):
|
||||
raise InvalidResponseError("recommendation is not a string")
|
||||
if recommendation not in (RECOMMENDATION_OK, RECOMMENDATION_SPAM):
|
||||
logger.warning(
|
||||
"get_pdu_policy_recommendation: unknown recommendation: %s",
|
||||
recommendation,
|
||||
)
|
||||
return RECOMMENDATION_OK
|
||||
return recommendation
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"get_pdu_policy_recommendation: server %s responded with error, assuming OK recommendation: %s",
|
||||
destination,
|
||||
e,
|
||||
)
|
||||
return RECOMMENDATION_OK
|
||||
|
||||
@trace
|
||||
@tag_args
|
||||
async def get_pdu(
|
||||
|
||||
@@ -701,6 +701,12 @@ class FederationServer(FederationBase):
|
||||
pdu = event_from_pdu_json(content, room_version)
|
||||
origin_host, _ = parse_server_name(origin)
|
||||
await self.check_server_matches_acl(origin_host, pdu.room_id)
|
||||
if await self._spam_checker_module_callbacks.should_drop_federated_event(pdu):
|
||||
logger.info(
|
||||
"Federated event contains spam, dropping %s",
|
||||
pdu.event_id,
|
||||
)
|
||||
raise SynapseError(403, Codes.FORBIDDEN)
|
||||
try:
|
||||
pdu = await self._check_sigs_and_hash(room_version, pdu)
|
||||
except InvalidEventSignatureError as e:
|
||||
|
||||
@@ -143,6 +143,33 @@ class TransportLayerClient:
|
||||
destination, path=path, timeout=timeout, try_trailing_slash_on_400=True
|
||||
)
|
||||
|
||||
async def get_policy_recommendation_for_pdu(
|
||||
self, destination: str, event: EventBase, timeout: Optional[int] = None
|
||||
) -> JsonDict:
|
||||
"""Requests the policy recommendation for the given pdu from the given policy server.
|
||||
|
||||
Args:
|
||||
destination: The host name of the remote homeserver checking the event.
|
||||
event: The event to check.
|
||||
timeout: How long to try (in ms) the destination for before giving up.
|
||||
None indicates no timeout.
|
||||
|
||||
Returns:
|
||||
The full recommendation object from the remote server.
|
||||
"""
|
||||
logger.debug(
|
||||
"get_policy_recommendation_for_pdu dest=%s, event_id=%s",
|
||||
destination,
|
||||
event.event_id,
|
||||
)
|
||||
return await self.client.post_json(
|
||||
destination=destination,
|
||||
path=f"/_matrix/policy/unstable/org.matrix.msc4284/event/{event.event_id}/check",
|
||||
data=event.get_pdu_json(),
|
||||
ignore_backoff=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
async def backfill(
|
||||
self, destination: str, room_id: str, event_tuples: Collection[str], limit: int
|
||||
) -> Optional[Union[JsonDict, list]]:
|
||||
|
||||
@@ -445,7 +445,7 @@ class AdminHandler:
|
||||
user_id,
|
||||
room,
|
||||
limit,
|
||||
["m.room.member", "m.room.message"],
|
||||
["m.room.member", "m.room.message", "m.room.encrypted"],
|
||||
)
|
||||
if not event_ids:
|
||||
# nothing to redact in this room
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#
|
||||
#
|
||||
import logging
|
||||
from threading import Lock
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AbstractSet,
|
||||
@@ -1237,7 +1238,7 @@ class DeviceListUpdater(DeviceListWorkerUpdater):
|
||||
)
|
||||
|
||||
# Attempt to resync out of sync device lists every 30s.
|
||||
self._resync_retry_in_progress = False
|
||||
self._resync_retry_lock = Lock()
|
||||
self.clock.looping_call(
|
||||
run_as_background_process,
|
||||
30 * 1000,
|
||||
@@ -1419,13 +1420,10 @@ class DeviceListUpdater(DeviceListWorkerUpdater):
|
||||
"""Retry to resync device lists that are out of sync, except if another retry is
|
||||
in progress.
|
||||
"""
|
||||
if self._resync_retry_in_progress:
|
||||
# If the lock can not be acquired we want to always return immediately instead of blocking here
|
||||
if not self._resync_retry_lock.acquire(blocking=False):
|
||||
return
|
||||
|
||||
try:
|
||||
# Prevent another call of this function to retry resyncing device lists so
|
||||
# we don't send too many requests.
|
||||
self._resync_retry_in_progress = True
|
||||
# Get all of the users that need resyncing.
|
||||
need_resync = await self.store.get_user_ids_requiring_device_list_resync()
|
||||
|
||||
@@ -1466,8 +1464,7 @@ class DeviceListUpdater(DeviceListWorkerUpdater):
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
# Allow future calls to retry resyncinc out of sync device lists.
|
||||
self._resync_retry_in_progress = False
|
||||
self._resync_retry_lock.release()
|
||||
|
||||
async def multi_user_device_resync(
|
||||
self, user_ids: List[str], mark_failed_as_stale: bool = True
|
||||
|
||||
@@ -158,7 +158,37 @@ class E2eKeysHandler:
|
||||
the number of in-flight queries at a time.
|
||||
"""
|
||||
async with self._query_devices_linearizer.queue((from_user_id, from_device_id)):
|
||||
device_keys_query: Dict[str, List[str]] = query_body.get("device_keys", {})
|
||||
|
||||
async def filter_device_key_query(
|
||||
query: Dict[str, List[str]],
|
||||
) -> Dict[str, List[str]]:
|
||||
if not self.config.experimental.msc4263_limit_key_queries_to_users_who_share_rooms:
|
||||
# Only ignore invalid user IDs, which is the same behaviour as if
|
||||
# the user existed but had no keys.
|
||||
return {
|
||||
user_id: v
|
||||
for user_id, v in query.items()
|
||||
if UserID.is_valid(user_id)
|
||||
}
|
||||
|
||||
# Strip invalid user IDs and user IDs the requesting user does not share rooms with.
|
||||
valid_user_ids = [
|
||||
user_id for user_id in query.keys() if UserID.is_valid(user_id)
|
||||
]
|
||||
allowed_user_ids = set(
|
||||
await self.store.do_users_share_a_room_joined_or_invited(
|
||||
from_user_id, valid_user_ids
|
||||
)
|
||||
)
|
||||
return {
|
||||
user_id: v
|
||||
for user_id, v in query.items()
|
||||
if user_id in allowed_user_ids
|
||||
}
|
||||
|
||||
device_keys_query: Dict[str, List[str]] = await filter_device_key_query(
|
||||
query_body.get("device_keys", {})
|
||||
)
|
||||
|
||||
# separate users by domain.
|
||||
# make a map from domain to user_id to device_ids
|
||||
@@ -166,11 +196,6 @@ class E2eKeysHandler:
|
||||
remote_queries = {}
|
||||
|
||||
for user_id, device_ids in device_keys_query.items():
|
||||
if not UserID.is_valid(user_id):
|
||||
# Ignore invalid user IDs, which is the same behaviour as if
|
||||
# the user existed but had no keys.
|
||||
continue
|
||||
|
||||
# we use UserID.from_string to catch invalid user ids
|
||||
if self.is_mine(UserID.from_string(user_id)):
|
||||
local_query[user_id] = device_ids
|
||||
@@ -1163,7 +1188,7 @@ class E2eKeysHandler:
|
||||
devices = devices[user_id]
|
||||
except SynapseError as e:
|
||||
failure = _exception_to_failure(e)
|
||||
failures[user_id] = {device: failure for device in signatures.keys()}
|
||||
failures[user_id] = dict.fromkeys(signatures.keys(), failure)
|
||||
return signature_list, failures
|
||||
|
||||
for device_id, device in signatures.items():
|
||||
@@ -1303,7 +1328,7 @@ class E2eKeysHandler:
|
||||
except SynapseError as e:
|
||||
failure = _exception_to_failure(e)
|
||||
for user, devicemap in signatures.items():
|
||||
failures[user] = {device_id: failure for device_id in devicemap.keys()}
|
||||
failures[user] = dict.fromkeys(devicemap.keys(), failure)
|
||||
return signature_list, failures
|
||||
|
||||
for target_user, devicemap in signatures.items():
|
||||
@@ -1344,9 +1369,7 @@ class E2eKeysHandler:
|
||||
# other devices were signed -- mark those as failures
|
||||
logger.debug("upload signature: too many devices specified")
|
||||
failure = _exception_to_failure(NotFoundError("Unknown device"))
|
||||
failures[target_user] = {
|
||||
device: failure for device in other_devices
|
||||
}
|
||||
failures[target_user] = dict.fromkeys(other_devices, failure)
|
||||
|
||||
if user_signing_key_id in master_key.get("signatures", {}).get(
|
||||
user_id, {}
|
||||
@@ -1367,9 +1390,7 @@ class E2eKeysHandler:
|
||||
except SynapseError as e:
|
||||
failure = _exception_to_failure(e)
|
||||
if device_id is None:
|
||||
failures[target_user] = {
|
||||
device_id: failure for device_id in devicemap.keys()
|
||||
}
|
||||
failures[target_user] = dict.fromkeys(devicemap.keys(), failure)
|
||||
else:
|
||||
failures.setdefault(target_user, {})[device_id] = failure
|
||||
|
||||
|
||||
@@ -1312,9 +1312,9 @@ class FederationHandler:
|
||||
if state_key is not None:
|
||||
# the event was not rejected (get_event raises a NotFoundError for rejected
|
||||
# events) so the state at the event should include the event itself.
|
||||
assert (
|
||||
state_map.get((event.type, state_key)) == event.event_id
|
||||
), "State at event did not include event itself"
|
||||
assert state_map.get((event.type, state_key)) == event.event_id, (
|
||||
"State at event did not include event itself"
|
||||
)
|
||||
|
||||
# ... but we need the state *before* that event
|
||||
if "replaces_state" in event.unsigned:
|
||||
|
||||
@@ -143,9 +143,9 @@ class MessageHandler:
|
||||
elif membership == Membership.LEAVE:
|
||||
key = (event_type, state_key)
|
||||
# If the membership is not JOIN, then the event ID should exist.
|
||||
assert (
|
||||
membership_event_id is not None
|
||||
), "check_user_in_room_or_world_readable returned invalid data"
|
||||
assert membership_event_id is not None, (
|
||||
"check_user_in_room_or_world_readable returned invalid data"
|
||||
)
|
||||
room_state = await self._state_storage_controller.get_state_for_events(
|
||||
[membership_event_id], StateFilter.from_types([key])
|
||||
)
|
||||
@@ -242,9 +242,9 @@ class MessageHandler:
|
||||
room_state = await self.store.get_events(state_ids.values())
|
||||
elif membership == Membership.LEAVE:
|
||||
# If the membership is not JOIN, then the event ID should exist.
|
||||
assert (
|
||||
membership_event_id is not None
|
||||
), "check_user_in_room_or_world_readable returned invalid data"
|
||||
assert membership_event_id is not None, (
|
||||
"check_user_in_room_or_world_readable returned invalid data"
|
||||
)
|
||||
room_state_events = (
|
||||
await self._state_storage_controller.get_state_for_events(
|
||||
[membership_event_id], state_filter=state_filter
|
||||
@@ -495,6 +495,7 @@ class EventCreationHandler:
|
||||
self._instance_name = hs.get_instance_name()
|
||||
self._notifier = hs.get_notifier()
|
||||
self._worker_lock_handler = hs.get_worker_locks_handler()
|
||||
self._policy_handler = hs.get_room_policy_handler()
|
||||
|
||||
self.room_prejoin_state_types = self.hs.config.api.room_prejoin_state
|
||||
|
||||
@@ -1108,6 +1109,18 @@ class EventCreationHandler:
|
||||
event.sender,
|
||||
)
|
||||
|
||||
policy_allowed = await self._policy_handler.is_event_allowed(event)
|
||||
if not policy_allowed:
|
||||
logger.warning(
|
||||
"Event not allowed by policy server, rejecting %s",
|
||||
event.event_id,
|
||||
)
|
||||
raise SynapseError(
|
||||
403,
|
||||
"This message has been rejected as probable spam",
|
||||
Codes.FORBIDDEN,
|
||||
)
|
||||
|
||||
spam_check_result = (
|
||||
await self._spam_checker_module_callbacks.check_event_for_spam(
|
||||
event
|
||||
@@ -1119,7 +1132,7 @@ class EventCreationHandler:
|
||||
[code, dict] = spam_check_result
|
||||
raise SynapseError(
|
||||
403,
|
||||
"This message had been rejected as probable spam",
|
||||
"This message has been rejected as probable spam",
|
||||
code,
|
||||
dict,
|
||||
)
|
||||
@@ -1266,12 +1279,14 @@ class EventCreationHandler:
|
||||
# Allow an event to have empty list of prev_event_ids
|
||||
# only if it has auth_event_ids.
|
||||
or auth_event_ids
|
||||
), "Attempting to create a non-m.room.create event with no prev_events or auth_event_ids"
|
||||
), (
|
||||
"Attempting to create a non-m.room.create event with no prev_events or auth_event_ids"
|
||||
)
|
||||
else:
|
||||
# we now ought to have some prev_events (unless it's a create event).
|
||||
assert (
|
||||
builder.type == EventTypes.Create or prev_event_ids
|
||||
), "Attempting to create a non-m.room.create event with no prev_events"
|
||||
assert builder.type == EventTypes.Create or prev_event_ids, (
|
||||
"Attempting to create a non-m.room.create event with no prev_events"
|
||||
)
|
||||
|
||||
if for_batch:
|
||||
assert prev_event_ids is not None
|
||||
|
||||
@@ -586,6 +586,24 @@ class OidcProvider:
|
||||
or self._user_profile_method == "userinfo_endpoint"
|
||||
)
|
||||
|
||||
@property
|
||||
def _uses_access_token(self) -> bool:
|
||||
"""Return True if the `access_token` will be used during the login process.
|
||||
|
||||
This is useful to determine whether the access token
|
||||
returned by the identity provider, and
|
||||
any related metadata (such as the `at_hash` field in
|
||||
the ID token), should be validated.
|
||||
"""
|
||||
# Currently, Synapse only uses the access_token to fetch user metadata
|
||||
# from the userinfo endpoint. Therefore we only have a single criteria
|
||||
# to check right now but this may change in the future and this function
|
||||
# should be updated if more usages are introduced.
|
||||
#
|
||||
# For example, if we start to use the access_token given to us by the
|
||||
# IdP for more things, such as accessing Resource Server APIs.
|
||||
return self._uses_userinfo
|
||||
|
||||
@property
|
||||
def issuer(self) -> str:
|
||||
"""The issuer identifying this provider."""
|
||||
@@ -957,9 +975,16 @@ class OidcProvider:
|
||||
"nonce": nonce,
|
||||
"client_id": self._client_auth.client_id,
|
||||
}
|
||||
if "access_token" in token:
|
||||
if self._uses_access_token and "access_token" in token:
|
||||
# If we got an `access_token`, there should be an `at_hash` claim
|
||||
# in the `id_token` that we can check against.
|
||||
# in the `id_token` that we can check against. Setting this
|
||||
# instructs authlib to check the value of `at_hash` in the
|
||||
# ID token.
|
||||
#
|
||||
# We only need to verify the access token if we actually make
|
||||
# use of it. Which currently only happens when we need to fetch
|
||||
# the user's information from the userinfo_endpoint. Thus, this
|
||||
# check is also gated on self._uses_userinfo.
|
||||
claims_params["access_token"] = token["access_token"]
|
||||
|
||||
claims_options = {"iss": {"values": [metadata["issuer"]]}}
|
||||
|
||||
@@ -159,7 +159,10 @@ class RegistrationHandler:
|
||||
if not localpart:
|
||||
raise SynapseError(400, "User ID cannot be empty", Codes.INVALID_USERNAME)
|
||||
|
||||
if localpart[0] == "_":
|
||||
if (
|
||||
localpart[0] == "_"
|
||||
and not self.hs.config.registration.allow_underscore_prefixed_localpart
|
||||
):
|
||||
raise SynapseError(
|
||||
400, "User ID may not begin with _", Codes.INVALID_USERNAME
|
||||
)
|
||||
|
||||
@@ -1806,7 +1806,7 @@ class RoomShutdownHandler:
|
||||
] = None,
|
||||
) -> Optional[ShutdownRoomResponse]:
|
||||
"""
|
||||
Shuts down a room. Moves all local users and room aliases automatically
|
||||
Shuts down a room. Moves all joined local users and room aliases automatically
|
||||
to a new room if `new_room_user_id` is set. Otherwise local users only
|
||||
leave the room without any information.
|
||||
|
||||
@@ -1949,16 +1949,17 @@ class RoomShutdownHandler:
|
||||
|
||||
# Join users to new room
|
||||
if new_room_user_id:
|
||||
assert new_room_id is not None
|
||||
await self.room_member_handler.update_membership(
|
||||
requester=target_requester,
|
||||
target=target_requester.user,
|
||||
room_id=new_room_id,
|
||||
action=Membership.JOIN,
|
||||
content={},
|
||||
ratelimit=False,
|
||||
require_consent=False,
|
||||
)
|
||||
if membership == Membership.JOIN:
|
||||
assert new_room_id is not None
|
||||
await self.room_member_handler.update_membership(
|
||||
requester=target_requester,
|
||||
target=target_requester.user,
|
||||
room_id=new_room_id,
|
||||
action=Membership.JOIN,
|
||||
content={},
|
||||
ratelimit=False,
|
||||
require_consent=False,
|
||||
)
|
||||
|
||||
result["kicked_users"].append(user_id)
|
||||
if update_result_fct:
|
||||
|
||||
89
synapse/handlers/room_policy.py
Normal file
89
synapse/handlers/room_policy.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
||||
# Copyright (C) 2023 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from synapse.events import EventBase
|
||||
from synapse.types.handlers.policy_server import RECOMMENDATION_OK
|
||||
from synapse.util.stringutils import parse_and_validate_server_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RoomPolicyHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._hs = hs
|
||||
self._store = hs.get_datastores().main
|
||||
self._storage_controllers = hs.get_storage_controllers()
|
||||
self._event_auth_handler = hs.get_event_auth_handler()
|
||||
self._federation_client = hs.get_federation_client()
|
||||
|
||||
async def is_event_allowed(self, event: EventBase) -> bool:
|
||||
"""Check if the given event is allowed in the room by the policy server.
|
||||
|
||||
Note: This will *always* return True if the room's policy server is Synapse
|
||||
itself. This is because Synapse can't be a policy server (currently).
|
||||
|
||||
If no policy server is configured in the room, this returns True. Similarly, if
|
||||
the policy server is invalid in any way (not joined, not a server, etc), this
|
||||
returns True.
|
||||
|
||||
If a valid and contactable policy server is configured in the room, this returns
|
||||
True if that server suggests the event is not spammy, and False otherwise.
|
||||
|
||||
Args:
|
||||
event: The event to check. This should be a fully-formed PDU.
|
||||
|
||||
Returns:
|
||||
bool: True if the event is allowed in the room, False otherwise.
|
||||
"""
|
||||
policy_event = await self._storage_controllers.state.get_current_state_event(
|
||||
event.room_id, "org.matrix.msc4284.policy", ""
|
||||
)
|
||||
if not policy_event:
|
||||
return True # no policy server == default allow
|
||||
|
||||
policy_server = policy_event.content.get("via", "")
|
||||
if policy_server is None or not isinstance(policy_server, str):
|
||||
return True # no policy server == default allow
|
||||
|
||||
if policy_server == self._hs.hostname:
|
||||
return True # Synapse itself can't be a policy server (currently)
|
||||
|
||||
try:
|
||||
parse_and_validate_server_name(policy_server)
|
||||
except ValueError:
|
||||
return True # invalid policy server == default allow
|
||||
|
||||
is_in_room = await self._event_auth_handler.is_host_in_room(
|
||||
event.room_id, policy_server
|
||||
)
|
||||
if not is_in_room:
|
||||
return True # policy server not in room == default allow
|
||||
|
||||
# At this point, the server appears valid and is in the room, so ask it to check
|
||||
# the event.
|
||||
recommendation = await self._federation_client.get_pdu_policy_recommendation(
|
||||
policy_server, event
|
||||
)
|
||||
if recommendation != RECOMMENDATION_OK:
|
||||
return False
|
||||
|
||||
return True # default allow
|
||||
@@ -36,10 +36,17 @@ class SetPasswordHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.store = hs.get_datastores().main
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
# This can only be instantiated on the main process.
|
||||
device_handler = hs.get_device_handler()
|
||||
assert isinstance(device_handler, DeviceHandler)
|
||||
self._device_handler = device_handler
|
||||
|
||||
# We don't need the device handler if password changing is disabled.
|
||||
# This allows us to instantiate the SetPasswordHandler on the workers
|
||||
# that have admin APIs for MAS
|
||||
if self._auth_handler.can_change_password():
|
||||
# This can only be instantiated on the main process.
|
||||
device_handler = hs.get_device_handler()
|
||||
assert isinstance(device_handler, DeviceHandler)
|
||||
self._device_handler: Optional[DeviceHandler] = device_handler
|
||||
else:
|
||||
self._device_handler = None
|
||||
|
||||
async def set_password(
|
||||
self,
|
||||
@@ -51,6 +58,9 @@ class SetPasswordHandler:
|
||||
if not self._auth_handler.can_change_password():
|
||||
raise SynapseError(403, "Password change disabled", errcode=Codes.FORBIDDEN)
|
||||
|
||||
# We should have this available only if password changing is enabled.
|
||||
assert self._device_handler is not None
|
||||
|
||||
try:
|
||||
await self.store.user_set_password_hash(user_id, password_hash)
|
||||
except StoreError as e:
|
||||
|
||||
@@ -271,6 +271,7 @@ class SlidingSyncHandler:
|
||||
from_token=from_token,
|
||||
to_token=to_token,
|
||||
newly_joined=room_id in interested_rooms.newly_joined_rooms,
|
||||
newly_left=room_id in interested_rooms.newly_left_rooms,
|
||||
is_dm=room_id in interested_rooms.dm_room_ids,
|
||||
)
|
||||
|
||||
@@ -542,6 +543,7 @@ class SlidingSyncHandler:
|
||||
from_token: Optional[SlidingSyncStreamToken],
|
||||
to_token: StreamToken,
|
||||
newly_joined: bool,
|
||||
newly_left: bool,
|
||||
is_dm: bool,
|
||||
) -> SlidingSyncResult.RoomResult:
|
||||
"""
|
||||
@@ -559,6 +561,7 @@ class SlidingSyncHandler:
|
||||
from_token: The point in the stream to sync from.
|
||||
to_token: The point in the stream to sync up to.
|
||||
newly_joined: If the user has newly joined the room
|
||||
newly_left: If the user has newly left the room
|
||||
is_dm: Whether the room is a DM room
|
||||
"""
|
||||
user = sync_config.user
|
||||
@@ -856,6 +859,26 @@ class SlidingSyncHandler:
|
||||
# TODO: Limit the number of state events we're about to send down
|
||||
# the room, if its too many we should change this to an
|
||||
# `initial=True`?
|
||||
|
||||
# For the case of rejecting remote invites, the leave event won't be
|
||||
# returned by `get_current_state_deltas_for_room`. This is due to the current
|
||||
# state only being filled out for rooms the server is in, and so doesn't pick
|
||||
# up out-of-band leaves (including locally rejected invites) as these events
|
||||
# are outliers and not added to the `current_state_delta_stream`.
|
||||
#
|
||||
# We rely on being explicitly told that the room has been `newly_left` to
|
||||
# ensure we extract the out-of-band leave.
|
||||
if newly_left and room_membership_for_user_at_to_token.event_id is not None:
|
||||
membership_changed = True
|
||||
leave_event = await self.store.get_event(
|
||||
room_membership_for_user_at_to_token.event_id
|
||||
)
|
||||
state_key = leave_event.get_state_key()
|
||||
if state_key is not None:
|
||||
room_state_delta_id_map[(leave_event.type, state_key)] = (
|
||||
room_membership_for_user_at_to_token.event_id
|
||||
)
|
||||
|
||||
deltas = await self.get_current_state_deltas_for_room(
|
||||
room_id=room_id,
|
||||
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user