mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-07 01:20:16 +00:00
Compare commits
356 Commits
shay/add_c
...
dinsic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84e695f506 | ||
|
|
91694227b6 | ||
|
|
7a95d8fff9 | ||
|
|
99eec6d2d5 | ||
|
|
e5df12ad16 | ||
|
|
70714595bc | ||
|
|
62f5e3bc4c | ||
|
|
63d0d21a28 | ||
|
|
8551b4f336 | ||
|
|
359aed4168 | ||
|
|
cd47482257 | ||
|
|
a9567ee1a6 | ||
|
|
9502bd8d78 | ||
|
|
235271be4f | ||
|
|
0c6500a08b | ||
|
|
c862d5baf6 | ||
|
|
35ec13baab | ||
|
|
c4e56a8ee9 | ||
|
|
d2bb51080e | ||
|
|
0fda4e2e50 | ||
|
|
8ced9a2f58 | ||
|
|
bbd6208b3e | ||
|
|
36c61df659 | ||
|
|
132887db8c | ||
|
|
bec6d9e090 | ||
|
|
8a47155763 | ||
|
|
ffa30b507f | ||
|
|
00b47fdfc7 | ||
|
|
2526b79ce6 | ||
|
|
aea03c9d73 | ||
|
|
ddf256c77f | ||
|
|
dd92685179 | ||
|
|
0bb375c124 | ||
|
|
ea5f86304e | ||
|
|
d1a78ba2a3 | ||
|
|
dcdfdb3f17 | ||
|
|
4dc2eda1d3 | ||
|
|
13ad3f5ab1 | ||
|
|
28803ad56a | ||
|
|
446fb64d50 | ||
|
|
34dd738e4b | ||
|
|
048f86ef5d | ||
|
|
3bc4ea841d | ||
|
|
4e954f4bbd | ||
|
|
f10b6f7385 | ||
|
|
9fc40f7b3f | ||
|
|
cf22aae355 | ||
|
|
6b9d27d793 | ||
|
|
25fff95448 | ||
|
|
5fe0cea37e | ||
|
|
95b5d4ad54 | ||
|
|
4dd7de17b7 | ||
|
|
8b44097771 | ||
|
|
6b83a1826c | ||
|
|
aeda7fde78 | ||
|
|
d085e0df2a | ||
|
|
8d28817eac | ||
|
|
aa3ba41933 | ||
|
|
724ddaddb3 | ||
|
|
8636ec042b | ||
|
|
78b737ff71 | ||
|
|
a7a8bbf502 | ||
|
|
f304996c61 | ||
|
|
7c644198c8 | ||
|
|
ee0ee97447 | ||
|
|
f370fd01ec | ||
|
|
1ef3627b05 | ||
|
|
fbc5b58d21 | ||
|
|
84875301b6 | ||
|
|
18f0b69c49 | ||
|
|
4875746d82 | ||
|
|
fa4efb5967 | ||
|
|
c1bc48f9d4 | ||
|
|
9b3c69f661 | ||
|
|
7157276124 | ||
|
|
1532369dcd | ||
|
|
145291108d | ||
|
|
cefc5542fb | ||
|
|
2a1f35193b | ||
|
|
a09767d57d | ||
|
|
bc0fd8f170 | ||
|
|
d36a876d2d | ||
|
|
f7339d42ee | ||
|
|
8aea2c3be1 | ||
|
|
1cd0ecc1f2 | ||
|
|
19a4298a51 | ||
|
|
073dd7778e | ||
|
|
00736c8935 | ||
|
|
112a48a5aa | ||
|
|
53aff08ebe | ||
|
|
8b2f655589 | ||
|
|
bd8448ccb2 | ||
|
|
110608d1d6 | ||
|
|
5a28504c08 | ||
|
|
9bb7cec90f | ||
|
|
8a8644045f | ||
|
|
08d4534771 | ||
|
|
c8349787b7 | ||
|
|
e5e690041d | ||
|
|
fb1b76ff4c | ||
|
|
40ea934de0 | ||
|
|
5c4296b47a | ||
|
|
8b2543c0ec | ||
|
|
bd59e154c1 | ||
|
|
66378d13ce | ||
|
|
65d3652cb7 | ||
|
|
c644755123 | ||
|
|
c7964e7430 | ||
|
|
3c9bb86fde | ||
|
|
304a1376c2 | ||
|
|
aded5cfb72 | ||
|
|
57bd5cfc9a | ||
|
|
ad566df746 | ||
|
|
c4759cd041 | ||
|
|
4dda68da9a | ||
|
|
adf3eee668 | ||
|
|
a6cf1e079e | ||
|
|
996cf15d42 | ||
|
|
27cd0b3037 | ||
|
|
4f68188d0b | ||
|
|
26a27e06b9 | ||
|
|
5283f65316 | ||
|
|
4a1b749582 | ||
|
|
e0c56d6527 | ||
|
|
7c2d936802 | ||
|
|
1c7628eb3a | ||
|
|
8400e5f624 | ||
|
|
02a659ae30 | ||
|
|
a55b2efca1 | ||
|
|
d907e8f599 | ||
|
|
97174780ce | ||
|
|
9532eb55ec | ||
|
|
a766c41d25 | ||
|
|
426218323b | ||
|
|
453aaaadc0 | ||
|
|
10383e6e6f | ||
|
|
3c8262b181 | ||
|
|
389aa20170 | ||
|
|
c10226581a | ||
|
|
10510f1e7e | ||
|
|
9ece96f5c8 | ||
|
|
1924848dfa | ||
|
|
deee82aebc | ||
|
|
0e63dd89a4 | ||
|
|
d331119758 | ||
|
|
9cce175bf0 | ||
|
|
4f0e9a3f01 | ||
|
|
35eea39c8c | ||
|
|
8d16321edc | ||
|
|
028f674cd3 | ||
|
|
4914a88829 | ||
|
|
ccbc9e5e17 | ||
|
|
d51ca9d9b3 | ||
|
|
fe13bd52ac | ||
|
|
116f7778f4 | ||
|
|
d8fdba7bfb | ||
|
|
dde07c6859 | ||
|
|
64365fcbdd | ||
|
|
4a9eba9576 | ||
|
|
50cd07a836 | ||
|
|
7dfc3c327c | ||
|
|
42cea6b437 | ||
|
|
d9105b5ed8 | ||
|
|
6fbf2ae9a8 | ||
|
|
ed2b5b77f2 | ||
|
|
6d54f1534a | ||
|
|
d6e2f9f9da | ||
|
|
f608ddbe5c | ||
|
|
38e3d9ed67 | ||
|
|
aa5c42f5bc | ||
|
|
8bcb78891e | ||
|
|
f304f1a574 | ||
|
|
532ba44add | ||
|
|
10e3ed83e9 | ||
|
|
622ecec24b | ||
|
|
09ea63ae7a | ||
|
|
f059a91085 | ||
|
|
66f7588f87 | ||
|
|
b4f3d70b21 | ||
|
|
3d031c211d | ||
|
|
371296443f | ||
|
|
1973eb11d2 | ||
|
|
26c004129f | ||
|
|
0cc91efd6a | ||
|
|
fe6ac9c5d7 | ||
|
|
96bd70f6d0 | ||
|
|
3a9c405a0f | ||
|
|
909ceecc28 | ||
|
|
70da5202ba | ||
|
|
34bbbe81a6 | ||
|
|
2d979e639d | ||
|
|
d296cdc9dd | ||
|
|
8df16a8aee | ||
|
|
15b7a84aa8 | ||
|
|
2f61dd058d | ||
|
|
e6218e4880 | ||
|
|
7d71975e6a | ||
|
|
0fcf7e5c57 | ||
|
|
9bf49abc07 | ||
|
|
dd747ba045 | ||
|
|
b00a8d870c | ||
|
|
8fa09c7479 | ||
|
|
d94873d525 | ||
|
|
f2fa172375 | ||
|
|
8c0ebe3026 | ||
|
|
53dd358c83 | ||
|
|
f5c944c7f2 | ||
|
|
aefb7a1146 | ||
|
|
479b7b1eff | ||
|
|
f04ee0b351 | ||
|
|
164798ec32 | ||
|
|
b7d7d20a38 | ||
|
|
2bb6d85736 | ||
|
|
0f0671e5e0 | ||
|
|
8eb9f37a01 | ||
|
|
ea89e73ebf | ||
|
|
68a9d1fc34 | ||
|
|
feae387576 | ||
|
|
e64f7c0188 | ||
|
|
b85ff4b894 | ||
|
|
4eca8d3fb3 | ||
|
|
aa3dc78f65 | ||
|
|
de874364e7 | ||
|
|
7e94e2ad94 | ||
|
|
e7ec6f78ca | ||
|
|
7a9aa4b81b | ||
|
|
5827e976fe | ||
|
|
44c0661d97 | ||
|
|
85db96cc81 | ||
|
|
3271742905 | ||
|
|
0a23bf442f | ||
|
|
9b13038d05 | ||
|
|
dfe09ec313 | ||
|
|
13bc1e5307 | ||
|
|
70e039c7ae | ||
|
|
2712a9ef8f | ||
|
|
da757b7759 | ||
|
|
59bc7debf0 | ||
|
|
cf68593544 | ||
|
|
9cc95fd0a5 | ||
|
|
82886e4c8f | ||
|
|
08919847c1 | ||
|
|
c3acc45a87 | ||
|
|
ae5bb32ad0 | ||
|
|
7ed3232b08 | ||
|
|
6e7488ce11 | ||
|
|
41585e1340 | ||
|
|
9498cd3e7b | ||
|
|
c7503f8f33 | ||
|
|
9d8baa1595 | ||
|
|
4ff8486f0f | ||
|
|
2669e494e0 | ||
|
|
b6d8a808a4 | ||
|
|
0cb5d34756 | ||
|
|
650761666d | ||
|
|
aa2a4b4b42 | ||
|
|
022469d819 | ||
|
|
45d06c754a | ||
|
|
dbd0821c43 | ||
|
|
0476852fc6 | ||
|
|
1d11d9323d | ||
|
|
261e4f2542 | ||
|
|
11728561f3 | ||
|
|
9d57abcadd | ||
|
|
cb0bbde981 | ||
|
|
abc97bd1de | ||
|
|
ee238254a0 | ||
|
|
0125b5d002 | ||
|
|
fe265fe990 | ||
|
|
7735eee41d | ||
|
|
3d0faa39fb | ||
|
|
fd28d13e19 | ||
|
|
d18731e252 | ||
|
|
81beae30b8 | ||
|
|
11f1bace3c | ||
|
|
1e8cfc9e77 | ||
|
|
488ed3e444 | ||
|
|
c3ec84dbcd | ||
|
|
0783801659 | ||
|
|
9f2fd29c14 | ||
|
|
6372dff771 | ||
|
|
b3e346f40c | ||
|
|
fb47ce3e6a | ||
|
|
debf04556b | ||
|
|
907a62df28 | ||
|
|
41b987cbc5 | ||
|
|
5c74ab4064 | ||
|
|
06820250c9 | ||
|
|
383c4ae59c | ||
|
|
f639ac143d | ||
|
|
ad0424bab0 | ||
|
|
2992125561 | ||
|
|
ef56b6e27c | ||
|
|
53d6245529 | ||
|
|
25e471dac3 | ||
|
|
76fca1730e | ||
|
|
32e4420a66 | ||
|
|
79b2583f1b | ||
|
|
8a24c4eee5 | ||
|
|
f93cb7410d | ||
|
|
50d5a97c1b | ||
|
|
c06932a029 | ||
|
|
3a62cacfb0 | ||
|
|
4d55b16faa | ||
|
|
105709bf32 | ||
|
|
d7fad867fa | ||
|
|
8fddcf703e | ||
|
|
e2adb360eb | ||
|
|
47ed4a4aa7 | ||
|
|
7fafa838ae | ||
|
|
de341bec1b | ||
|
|
643c89d497 | ||
|
|
6554253f48 | ||
|
|
3add16df49 | ||
|
|
dde01efbcb | ||
|
|
22e416b726 | ||
|
|
b4b7c80181 | ||
|
|
5fc3477fd3 | ||
|
|
8743f42b49 | ||
|
|
7285afa4be | ||
|
|
b22a53e357 | ||
|
|
3c446d0a81 | ||
|
|
240e940c3f | ||
|
|
969ed2e49d | ||
|
|
1147ce7e18 | ||
|
|
0d2b7fdcec | ||
|
|
4e12b10c7c | ||
|
|
e654230a51 | ||
|
|
ef5193e0cb | ||
|
|
7b3959c7f3 | ||
|
|
2e4a6c5aab | ||
|
|
e3eb2cfe8b | ||
|
|
5c341c99f6 | ||
|
|
739d3500fe | ||
|
|
0e2d70e101 | ||
|
|
82c4fd7226 | ||
|
|
e446077478 | ||
|
|
d82c89ac22 | ||
|
|
75b25b3f1f | ||
|
|
1df10d8814 | ||
|
|
8f9340d248 | ||
|
|
c5034cd4b0 | ||
|
|
f7f937d051 | ||
|
|
e52b5d94a9 | ||
|
|
d90f27a21f | ||
|
|
03cf9710e3 | ||
|
|
1dcdd8d568 | ||
|
|
4344fb1faf | ||
|
|
846577ebde | ||
|
|
3869981227 | ||
|
|
fa80b492a5 | ||
|
|
c776c52eed | ||
|
|
b424c16f50 | ||
|
|
313a489fc9 | ||
|
|
4b090cb273 | ||
|
|
3f79378d4b |
@@ -1,116 +1,8 @@
|
||||
version: 2
|
||||
jobs:
|
||||
dockerhubuploadrelease:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:${CIRCLE_TAG}-py2 .
|
||||
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:${CIRCLE_TAG} -t matrixdotorg/synapse:${CIRCLE_TAG}-py3 --build-arg PYTHON_VERSION=3.6 .
|
||||
- run: docker login --username $DOCKER_HUB_USERNAME --password $DOCKER_HUB_PASSWORD
|
||||
- run: docker push matrixdotorg/synapse:${CIRCLE_TAG}
|
||||
- run: docker push matrixdotorg/synapse:${CIRCLE_TAG}-py2
|
||||
- run: docker push matrixdotorg/synapse:${CIRCLE_TAG}-py3
|
||||
dockerhubuploadlatest:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:latest-py2 .
|
||||
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:latest -t matrixdotorg/synapse:latest-py3 --build-arg PYTHON_VERSION=3.6 .
|
||||
- run: docker login --username $DOCKER_HUB_USERNAME --password $DOCKER_HUB_PASSWORD
|
||||
- run: docker push matrixdotorg/synapse:latest
|
||||
- run: docker push matrixdotorg/synapse:latest-py2
|
||||
- run: docker push matrixdotorg/synapse:latest-py3
|
||||
sytestpy2:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy2
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
sytestpy2postgres:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy2
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: POSTGRES=1 /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
sytestpy2merged:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy2
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: bash .circleci/merge_base_branch.sh
|
||||
- run: /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
sytestpy2postgresmerged:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy2
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: bash .circleci/merge_base_branch.sh
|
||||
- run: POSTGRES=1 /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
|
||||
sytestpy3:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy3
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
sytestpy3postgres:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy3
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: POSTGRES=1 /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
sytestpy3merged:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy3
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
- run: bash .circleci/merge_base_branch.sh
|
||||
- run: /synapse_sytest.sh
|
||||
- store_artifacts:
|
||||
path: /logs
|
||||
destination: logs
|
||||
- store_test_results:
|
||||
path: /logs
|
||||
sytestpy3postgresmerged:
|
||||
docker:
|
||||
- image: matrixdotorg/sytest-synapsepy3
|
||||
- image: matrixdotorg/sytest-synapse:dinsic
|
||||
working_directory: /src
|
||||
steps:
|
||||
- checkout
|
||||
@@ -126,45 +18,7 @@ workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- sytestpy2:
|
||||
filters:
|
||||
branches:
|
||||
only: /develop|master|release-.*/
|
||||
- sytestpy2postgres:
|
||||
filters:
|
||||
branches:
|
||||
only: /develop|master|release-.*/
|
||||
- sytestpy3:
|
||||
filters:
|
||||
branches:
|
||||
only: /develop|master|release-.*/
|
||||
- sytestpy3postgres:
|
||||
filters:
|
||||
branches:
|
||||
only: /develop|master|release-.*/
|
||||
- sytestpy2merged:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /develop|master|release-.*/
|
||||
- sytestpy2postgresmerged:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /develop|master|release-.*/
|
||||
- sytestpy3merged:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /develop|master|release-.*/
|
||||
- sytestpy3postgresmerged:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /develop|master|release-.*/
|
||||
- dockerhubuploadrelease:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9].[0-9]+.[0-9]+.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- dockerhubuploadlatest:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
|
||||
@@ -9,11 +9,12 @@ source $BASH_ENV
|
||||
|
||||
if [[ -z "${CIRCLE_PR_NUMBER}" ]]
|
||||
then
|
||||
echo "Can't figure out what the PR number is! Assuming merge target is develop."
|
||||
echo "Can't figure out what the PR number is! Assuming merge target is dinsic."
|
||||
|
||||
# It probably hasn't had a PR opened yet. Since all PRs land on develop, we
|
||||
# can probably assume it's based on it and will be merged into it.
|
||||
GITBASE="develop"
|
||||
# It probably hasn't had a PR opened yet. Since all PRs for dinsic land on
|
||||
# dinsic, we can probably assume it's based on it and will be merged into
|
||||
# it.
|
||||
GITBASE="dinsic"
|
||||
else
|
||||
# Get the reference, using the GitHub API
|
||||
GITBASE=`wget -O- https://api.github.com/repos/matrix-org/synapse/pulls/${CIRCLE_PR_NUMBER} | jq -r '.base.ref'`
|
||||
|
||||
21
CHANGES.md
21
CHANGES.md
@@ -1,3 +1,24 @@
|
||||
Synapse 1.0.0 (2019-06-11)
|
||||
==========================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix bug where attempting to send transactions with large number of EDUs can fail. ([\#5418](https://github.com/matrix-org/synapse/issues/5418))
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Expand the federation guide to include relevant content from the MSC1711 FAQ ([\#5419](https://github.com/matrix-org/synapse/issues/5419))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Move password reset links to /_matrix/client/unstable namespace. ([\#5424](https://github.com/matrix-org/synapse/issues/5424))
|
||||
|
||||
|
||||
Synapse 1.0.0rc3 (2019-06-10)
|
||||
=============================
|
||||
|
||||
|
||||
27
INSTALL.md
27
INSTALL.md
@@ -1,14 +1,14 @@
|
||||
* [Installing Synapse](#installing-synapse)
|
||||
* [Installing from source](#installing-from-source)
|
||||
* [Platform-Specific Instructions](#platform-specific-instructions)
|
||||
* [Troubleshooting Installation](#troubleshooting-installation)
|
||||
* [Prebuilt packages](#prebuilt-packages)
|
||||
* [Setting up Synapse](#setting-up-synapse)
|
||||
* [TLS certificates](#tls-certificates)
|
||||
* [Email](#email)
|
||||
* [Registering a user](#registering-a-user)
|
||||
* [Setting up a TURN server](#setting-up-a-turn-server)
|
||||
* [URL previews](#url-previews)
|
||||
- [Installing Synapse](#installing-synapse)
|
||||
- [Installing from source](#installing-from-source)
|
||||
- [Platform-Specific Instructions](#platform-specific-instructions)
|
||||
- [Troubleshooting Installation](#troubleshooting-installation)
|
||||
- [Prebuilt packages](#prebuilt-packages)
|
||||
- [Setting up Synapse](#setting-up-synapse)
|
||||
- [TLS certificates](#tls-certificates)
|
||||
- [Email](#email)
|
||||
- [Registering a user](#registering-a-user)
|
||||
- [Setting up a TURN server](#setting-up-a-turn-server)
|
||||
- [URL previews](#url-previews)
|
||||
|
||||
# Installing Synapse
|
||||
|
||||
@@ -395,8 +395,9 @@ To configure Synapse to expose an HTTPS port, you will need to edit
|
||||
instance, if using certbot, use `fullchain.pem` as your certificate, not
|
||||
`cert.pem`).
|
||||
|
||||
For those of you upgrading your TLS certificate for Synapse 1.0 compliance,
|
||||
please take a look at [our guide](docs/MSC1711_certificates_FAQ.md#configuring-certificates-for-compatibility-with-synapse-100).
|
||||
For a more detailed guide to configuring your server for federation, see
|
||||
[federate.md](docs/federate.md)
|
||||
|
||||
|
||||
## Email
|
||||
|
||||
|
||||
@@ -48,3 +48,8 @@ prune .buildkite
|
||||
|
||||
exclude jenkins*
|
||||
recursive-exclude jenkins *.sh
|
||||
|
||||
# FIXME: we shouldn't have these templates here
|
||||
recursive-include res/templates-dinsic *.css
|
||||
recursive-include res/templates-dinsic *.html
|
||||
recursive-include res/templates-dinsic *.txt
|
||||
|
||||
1
changelog.d/5083.feature
Normal file
1
changelog.d/5083.feature
Normal file
@@ -0,0 +1 @@
|
||||
Adds auth_profile_reqs option to require access_token to GET /profile endpoints on CS API.
|
||||
1
changelog.d/5098.misc
Normal file
1
changelog.d/5098.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add workarounds for pep-517 install errors.
|
||||
1
changelog.d/5214.feature
Normal file
1
changelog.d/5214.feature
Normal file
@@ -0,0 +1 @@
|
||||
Allow server admins to define and enforce a password policy (MSC2000).
|
||||
1
changelog.d/5363.feature
Normal file
1
changelog.d/5363.feature
Normal file
@@ -0,0 +1 @@
|
||||
Allow expired user to trigger renewal email sending manually.
|
||||
1
changelog.d/5378.misc
Normal file
1
changelog.d/5378.misc
Normal file
@@ -0,0 +1 @@
|
||||
Track deactivated accounts in the database.
|
||||
1
changelog.d/5394.bugfix
Normal file
1
changelog.d/5394.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a bug where deactivated users could receive renewal emails if the account validity feature is on.
|
||||
1
changelog.d/5416.misc
Normal file
1
changelog.d/5416.misc
Normal file
@@ -0,0 +1 @@
|
||||
Add unique index to the profile_replication_status table.
|
||||
@@ -1 +0,0 @@
|
||||
Fix bug where attempting to send transactions with large number of EDUs can fail.
|
||||
1
changelog.d/5420.feature
Normal file
1
changelog.d/5420.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add configuration option to hide new users from the user directory.
|
||||
1
changelog.d/5464.bugfix
Normal file
1
changelog.d/5464.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix missing invite state after exchanging 3PID invites over federaton.
|
||||
2
changelog.d/5465.misc
Normal file
2
changelog.d/5465.misc
Normal file
@@ -0,0 +1,2 @@
|
||||
Track deactivated accounts in the database.
|
||||
|
||||
1
changelog.d/5474.feature
Normal file
1
changelog.d/5474.feature
Normal file
@@ -0,0 +1 @@
|
||||
Allow server admins to define implementations of extra rules for allowing or denying incoming events.
|
||||
1
changelog.d/5477.feature
Normal file
1
changelog.d/5477.feature
Normal file
@@ -0,0 +1 @@
|
||||
Allow server admins to define implementations of extra rules for allowing or denying incoming events.
|
||||
1
changelog.d/5493.misc
Normal file
1
changelog.d/5493.misc
Normal file
@@ -0,0 +1 @@
|
||||
Track deactivated accounts in the database.
|
||||
1
changelog.d/5534.feature
Normal file
1
changelog.d/5534.feature
Normal file
@@ -0,0 +1 @@
|
||||
Split public rooms directory auth config in two settings, in order to manage client auth independently from the federation part of it. Obsoletes the "restrict_public_rooms_to_local_users" configuration setting. If "restrict_public_rooms_to_local_users" is set in the config, Synapse will act as if both new options are enabled, i.e. require authentication through the client API and deny federation requests.
|
||||
1
changelog.d/5576.bugfix
Normal file
1
changelog.d/5576.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a bug that would cause invited users to receive several emails for a single 3PID invite in case the inviter is rate limited.
|
||||
1
changelog.d/5610.feature
Normal file
1
changelog.d/5610.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement new custom event rules for power levels.
|
||||
1
changelog.d/5644.bugfix
Normal file
1
changelog.d/5644.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix newly-registered users not being able to lookup their own profile without joining a room.
|
||||
1
changelog.d/5702.bugfix
Normal file
1
changelog.d/5702.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix 3PID invite to invite association detection in the Tchap room access rules.
|
||||
1
changelog.d/5760.feature
Normal file
1
changelog.d/5760.feature
Normal file
@@ -0,0 +1 @@
|
||||
Force the access rule to be "restricted" if the join rule is "public".
|
||||
1
changelog.d/5780.misc
Normal file
1
changelog.d/5780.misc
Normal file
@@ -0,0 +1 @@
|
||||
Allow looping calls to be given arguments.
|
||||
1
changelog.d/5807.feature
Normal file
1
changelog.d/5807.feature
Normal file
@@ -0,0 +1 @@
|
||||
Allow defining HTML templates to serve the user on account renewal attempt when using the account validity feature.
|
||||
1
changelog.d/5815.feature
Normal file
1
changelog.d/5815.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement per-room message retention policies.
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
||||
matrix-synapse-py3 (1.0.0) stable; urgency=medium
|
||||
|
||||
* New synapse release 1.0.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 11 Jun 2019 17:09:53 +0100
|
||||
|
||||
matrix-synapse-py3 (0.99.5.2) stable; urgency=medium
|
||||
|
||||
* New synapse release 0.99.5.2.
|
||||
|
||||
@@ -14,7 +14,7 @@ This image is designed to run either with an automatically generated
|
||||
configuration file or with a custom configuration that requires manual editing.
|
||||
|
||||
An easy way to make use of this image is via docker-compose. See the
|
||||
[contrib/docker](../contrib/docker) section of the synapse project for
|
||||
[contrib/docker](https://github.com/matrix-org/synapse/tree/master/contrib/docker) section of the synapse project for
|
||||
examples.
|
||||
|
||||
### Without Compose (harder)
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# MSC1711 Certificates FAQ
|
||||
|
||||
## Historical Note
|
||||
This document was originally written to guide server admins through the upgrade
|
||||
path towards Synapse 1.0. Specifically,
|
||||
[MSC1711](https://github.com/matrix-org/matrix-doc/blob/master/proposals/1711-x509-for-federation.md)
|
||||
required that all servers present valid TLS certificates on their federation
|
||||
API. Admins were encouraged to achieve compliance from version 0.99.0 (released
|
||||
in February 2019) ahead of version 1.0 (released June 2019) enforcing the
|
||||
certificate checks.
|
||||
|
||||
Much of what follows is now outdated since most admins will have already
|
||||
upgraded, however it may be of use to those with old installs returning to the
|
||||
project.
|
||||
|
||||
If you are setting up a server from scratch you almost certainly should look at
|
||||
the [installation guide](INSTALL.md) instead.
|
||||
|
||||
## Introduction
|
||||
The goal of Synapse 0.99.0 is to act as a stepping stone to Synapse 1.0.0. It
|
||||
supports the r0.1 release of the server to server specification, but is
|
||||
compatible with both the legacy Matrix federation behaviour (pre-r0.1) as well
|
||||
|
||||
@@ -14,9 +14,9 @@ up and will work provided you set the ``server_name`` to match your
|
||||
machine's public DNS hostname, and provide Synapse with a TLS certificate
|
||||
which is valid for your ``server_name``.
|
||||
|
||||
Once you have completed the steps necessary to federate, you should be able to
|
||||
join a room via federation. (A good place to start is ``#synapse:matrix.org`` - a
|
||||
room for Synapse admins.)
|
||||
Once federation has been configured, you should be able to join a room over
|
||||
federation. A good place to start is ``#synapse:matrix.org`` - a room for
|
||||
Synapse admins.
|
||||
|
||||
|
||||
## Delegation
|
||||
@@ -98,6 +98,77 @@ _matrix._tcp.<server_name>``. In our example, we would expect this:
|
||||
Note that the target of a SRV record cannot be an alias (CNAME record): it has to point
|
||||
directly to the server hosting the synapse instance.
|
||||
|
||||
### Delegation FAQ
|
||||
#### When do I need a SRV record or .well-known URI?
|
||||
|
||||
If your homeserver listens on the default federation port (8448), and your
|
||||
`server_name` points to the host that your homeserver runs on, you do not need an SRV
|
||||
record or `.well-known/matrix/server` URI.
|
||||
|
||||
For instance, if you registered `example.com` and pointed its DNS A record at a
|
||||
fresh server, you could install Synapse on that host,
|
||||
giving it a `server_name` of `example.com`, and once [ACME](acme.md) support is enabled,
|
||||
it would automatically generate a valid TLS certificate for you via Let's Encrypt
|
||||
and no SRV record or .well-known URI would be needed.
|
||||
|
||||
This is the common case, although you can add an SRV record or
|
||||
`.well-known/matrix/server` URI for completeness if you wish.
|
||||
|
||||
**However**, if your server does not listen on port 8448, or if your `server_name`
|
||||
does not point to the host that your homeserver runs on, you will need to let
|
||||
other servers know how to find it. The way to do this is via .well-known or an
|
||||
SRV record.
|
||||
|
||||
#### I have created a .well-known URI. Do I still need an SRV record?
|
||||
|
||||
As of Synapse 0.99, Synapse will first check for the existence of a .well-known
|
||||
URI and follow any delegation it suggests. It will only then check for the
|
||||
existence of an SRV record.
|
||||
|
||||
That means that the SRV record will often be redundant. However, you should
|
||||
remember that there may still be older versions of Synapse in the federation
|
||||
which do not understand .well-known URIs, so if you removed your SRV record
|
||||
you would no longer be able to federate with them.
|
||||
|
||||
It is therefore best to leave the SRV record in place for now. Synapse 0.34 and
|
||||
earlier will follow the SRV record (and not care about the invalid
|
||||
certificate). Synapse 0.99 and later will follow the .well-known URI, with the
|
||||
correct certificate chain.
|
||||
|
||||
#### Can I manage my own certificates rather than having Synapse renew certificates itself?
|
||||
|
||||
Yes, you are welcome to manage your certificates yourself. Synapse will only
|
||||
attempt to obtain certificates from Let's Encrypt if you configure it to do
|
||||
so.The only requirement is that there is a valid TLS cert present for
|
||||
federation end points.
|
||||
|
||||
#### Do you still recommend against using a reverse proxy on the federation port?
|
||||
|
||||
We no longer actively recommend against using a reverse proxy. Many admins will
|
||||
find it easier to direct federation traffic to a reverse proxy and manage their
|
||||
own TLS certificates, and this is a supported configuration.
|
||||
|
||||
See [reverse_proxy.rst](reverse_proxy.rst) for information on setting up a
|
||||
reverse proxy.
|
||||
|
||||
#### Do I still need to give my TLS certificates to Synapse if I am using a reverse proxy?
|
||||
|
||||
Practically speaking, this is no longer necessary.
|
||||
|
||||
If you are using a reverse proxy for all of your TLS traffic, then you can set
|
||||
`no_tls: True` in the Synapse config. In that case, the only reason Synapse
|
||||
needs the certificate is to populate a legacy `tls_fingerprints` field in the
|
||||
federation API. This is ignored by Synapse 0.99.0 and later, and the only time
|
||||
pre-0.99 Synapses will check it is when attempting to fetch the server keys -
|
||||
and generally this is delegated via `matrix.org`, which will be running a modern
|
||||
version of Synapse.
|
||||
|
||||
#### Do I need the same certificate for the client and federation port?
|
||||
|
||||
No. There is nothing stopping you from using different certificates,
|
||||
particularly if you are using a reverse proxy. However, Synapse will use the
|
||||
same certificate on any ports where TLS is configured.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
You can use the [federation tester](
|
||||
|
||||
@@ -77,11 +77,15 @@ pid_file: DATADIR/homeserver.pid
|
||||
#
|
||||
#require_auth_for_profile_requests: true
|
||||
|
||||
# If set to 'true', requires authentication to access the server's
|
||||
# public rooms directory through the client API, and forbids any other
|
||||
# homeserver to fetch it via federation. Defaults to 'false'.
|
||||
# If set to 'false', requires authentication to access the server's public rooms
|
||||
# directory through the client API. Defaults to 'true'.
|
||||
#
|
||||
#restrict_public_rooms_to_local_users: true
|
||||
#allow_public_rooms_without_auth: false
|
||||
|
||||
# If set to 'false', forbids any other homeserver to fetch the server's public
|
||||
# rooms directory via federation. Defaults to 'true'.
|
||||
#
|
||||
#allow_public_rooms_over_federation: false
|
||||
|
||||
# The default room version for newly created rooms.
|
||||
#
|
||||
@@ -308,6 +312,74 @@ listeners:
|
||||
#
|
||||
#allow_per_room_profiles: false
|
||||
|
||||
# Whether to show the users on this homeserver in the user directory. Defaults to
|
||||
# 'true'.
|
||||
#
|
||||
#show_users_in_user_directory: false
|
||||
|
||||
# Message retention policy at the server level.
|
||||
#
|
||||
# Room admins and mods can define a retention period for their rooms using the
|
||||
# 'm.room.retention' state event, and server admins can cap this period by setting
|
||||
# the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options.
|
||||
#
|
||||
# If this feature is enabled, Synapse will regularly look for and purge events
|
||||
# which are older than the room's maximum retention period. Synapse will also
|
||||
# filter events received over federation so that events that should have been
|
||||
# purged are ignored and not stored again.
|
||||
#
|
||||
retention:
|
||||
# The message retention policies feature is disabled by default. Uncomment the
|
||||
# following line to enable it.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# Default retention policy. If set, Synapse will apply it to rooms that lack the
|
||||
# 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't
|
||||
# matter much because Synapse doesn't take it into account yet.
|
||||
#
|
||||
#default_policy:
|
||||
# min_lifetime: 1d
|
||||
# max_lifetime: 1y
|
||||
|
||||
# Retention policy limits. If set, a user won't be able to send a
|
||||
# 'm.room.retention' event which features a 'min_lifetime' or a 'max_lifetime'
|
||||
# that's not within this range. This is especially useful in closed federations,
|
||||
# in which server admins can make sure every federating server applies the same
|
||||
# rules.
|
||||
#
|
||||
#allowed_lifetime_min: 1d
|
||||
#allowed_lifetime_max: 1y
|
||||
|
||||
# Server admins can define the settings of the background jobs purging the
|
||||
# events which lifetime has expired under the 'purge_jobs' section.
|
||||
#
|
||||
# If no configuration is provided, a single job will be set up to delete expired
|
||||
# events in every room daily.
|
||||
#
|
||||
# Each job's configuration defines which range of message lifetimes the job
|
||||
# takes care of. For example, if 'shortest_max_lifetime' is '2d' and
|
||||
# 'longest_max_lifetime' is '3d', the job will handle purging expired events in
|
||||
# rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and
|
||||
# lower than or equal to 3 days. Both the minimum and the maximum value of a
|
||||
# range are optional, e.g. a job with no 'shortest_max_lifetime' and a
|
||||
# 'longest_max_lifetime' of '3d' will handle every room with a retention policy
|
||||
# which 'max_lifetime' is lower than or equal to three days.
|
||||
#
|
||||
# The rationale for this per-job configuration is that some rooms might have a
|
||||
# retention policy with a low 'max_lifetime', where history needs to be purged
|
||||
# of outdated messages on a very frequent basis (e.g. every 5min), but not want
|
||||
# that purge to be performed by a job that's iterating over every room it knows,
|
||||
# which would be quite heavy on the server.
|
||||
#
|
||||
#purge_jobs:
|
||||
# - shortest_max_lifetime: 1d
|
||||
# longest_max_lifetime: 3d
|
||||
# interval: 5m:
|
||||
# - shortest_max_lifetime: 3d
|
||||
# longest_max_lifetime: 1y
|
||||
# interval: 24h
|
||||
|
||||
|
||||
## TLS ##
|
||||
|
||||
@@ -788,6 +860,16 @@ uploads_path: "DATADIR/uploads"
|
||||
# period: 6w
|
||||
# renew_at: 1w
|
||||
# renew_email_subject: "Renew your %(app)s account"
|
||||
# # Directory in which Synapse will try to find the HTML files to serve to the
|
||||
# # user when trying to renew an account. Optional, defaults to
|
||||
# # synapse/res/templates.
|
||||
# template_dir: "res/templates"
|
||||
# # HTML to be displayed to the user after they successfully renewed their
|
||||
# # account. Optional.
|
||||
# account_renewed_html_path: "account_renewed.html"
|
||||
# # HTML to be displayed when the user tries to renew an account with an invalid
|
||||
# # renewal token. Optional.
|
||||
# invalid_token_html_path: "invalid_token.html"
|
||||
|
||||
# The user must provide all of the below types of 3PID when registering.
|
||||
#
|
||||
@@ -800,9 +882,32 @@ uploads_path: "DATADIR/uploads"
|
||||
#
|
||||
#disable_msisdn_registration: true
|
||||
|
||||
# Derive the user's matrix ID from a type of 3PID used when registering.
|
||||
# This overrides any matrix ID the user proposes when calling /register
|
||||
# The 3PID type should be present in registrations_require_3pid to avoid
|
||||
# users failing to register if they don't specify the right kind of 3pid.
|
||||
#
|
||||
#register_mxid_from_3pid: email
|
||||
|
||||
# Uncomment to set the display name of new users to their email address,
|
||||
# rather than using the default heuristic.
|
||||
#
|
||||
#register_just_use_email_for_display_name: true
|
||||
|
||||
# Mandate that users are only allowed to associate certain formats of
|
||||
# 3PIDs with accounts on this server.
|
||||
#
|
||||
# Use an Identity Server to establish which 3PIDs are allowed to register?
|
||||
# Overrides allowed_local_3pids below.
|
||||
#
|
||||
#check_is_for_allowed_local_3pids: matrix.org
|
||||
#
|
||||
# If you are using an IS you can also check whether that IS registers
|
||||
# pending invites for the given 3PID (and then allow it to sign up on
|
||||
# the platform):
|
||||
#
|
||||
#allow_invited_3pids: False
|
||||
#
|
||||
#allowed_local_3pids:
|
||||
# - medium: email
|
||||
# pattern: '.*@matrix\.org'
|
||||
@@ -811,6 +916,11 @@ uploads_path: "DATADIR/uploads"
|
||||
# - medium: msisdn
|
||||
# pattern: '\+44'
|
||||
|
||||
# If true, stop users from trying to change the 3PIDs associated with
|
||||
# their accounts.
|
||||
#
|
||||
#disable_3pid_changes: False
|
||||
|
||||
# Enable 3PIDs lookup requests to identity servers from this server.
|
||||
#
|
||||
#enable_3pid_lookup: true
|
||||
@@ -852,6 +962,30 @@ uploads_path: "DATADIR/uploads"
|
||||
# - matrix.org
|
||||
# - vector.im
|
||||
|
||||
# If enabled, user IDs, display names and avatar URLs will be replicated
|
||||
# to this server whenever they change.
|
||||
# This is an experimental API currently implemented by sydent to support
|
||||
# cross-homeserver user directories.
|
||||
#
|
||||
#replicate_user_profiles_to: example.com
|
||||
|
||||
# If specified, attempt to replay registrations, profile changes & 3pid
|
||||
# bindings on the given target homeserver via the AS API. The HS is authed
|
||||
# via a given AS token.
|
||||
#
|
||||
#shadow_server:
|
||||
# hs_url: https://shadow.example.com
|
||||
# hs: shadow.example.com
|
||||
# as_token: 12u394refgbdhivsia
|
||||
|
||||
# If enabled, don't let users set their own display names/avatars
|
||||
# other than for the very first time (unless they are a server admin).
|
||||
# Useful when provisioning users based on the contents of a 3rd party
|
||||
# directory and to avoid ambiguities.
|
||||
#
|
||||
#disable_set_displayname: False
|
||||
#disable_set_avatar_url: False
|
||||
|
||||
# Users who register on this homeserver will automatically be joined
|
||||
# to these rooms
|
||||
#
|
||||
@@ -1063,6 +1197,36 @@ password_config:
|
||||
#
|
||||
#pepper: "EVEN_MORE_SECRET"
|
||||
|
||||
# Define and enforce a password policy. Each parameter is optional, boolean
|
||||
# parameters default to 'false' and integer parameters default to 0.
|
||||
# This is an early implementation of MSC2000.
|
||||
#
|
||||
#policy:
|
||||
# Whether to enforce the password policy.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# Minimum accepted length for a password.
|
||||
#
|
||||
#minimum_length: 15
|
||||
|
||||
# Whether a password must contain at least one digit.
|
||||
#
|
||||
#require_digit: true
|
||||
|
||||
# Whether a password must contain at least one symbol.
|
||||
# A symbol is any character that's not a number or a letter.
|
||||
#
|
||||
#require_symbol: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_lowercase: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_uppercase: true
|
||||
|
||||
|
||||
|
||||
# Enable sending emails for password resets, notification events or
|
||||
@@ -1199,6 +1363,11 @@ password_config:
|
||||
#user_directory:
|
||||
# enabled: true
|
||||
# search_all_users: false
|
||||
#
|
||||
# # If this is set, user search will be delegated to this ID server instead
|
||||
# # of synapse performing the search itself.
|
||||
# # This is an experimental API.
|
||||
# defer_to_id_server: https://id.example.com
|
||||
|
||||
|
||||
# User Consent configuration
|
||||
|
||||
7
res/templates-dinsic/mail-Vector.css
Normal file
7
res/templates-dinsic/mail-Vector.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.header {
|
||||
border-bottom: 4px solid #e4f7ed ! important;
|
||||
}
|
||||
|
||||
.notif_link a, .footer a {
|
||||
color: #76CFA6 ! important;
|
||||
}
|
||||
156
res/templates-dinsic/mail.css
Normal file
156
res/templates-dinsic/mail.css
Normal file
@@ -0,0 +1,156 @@
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#page {
|
||||
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
|
||||
font-color: #454545;
|
||||
font-size: 12pt;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#inner {
|
||||
width: 640px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 87px;
|
||||
color: #454545;
|
||||
border-bottom: 4px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: right;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.salutation {
|
||||
padding-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.summarytext {
|
||||
}
|
||||
|
||||
.room {
|
||||
width: 100%;
|
||||
color: #454545;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.room_header td {
|
||||
padding-top: 38px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.room_name {
|
||||
vertical-align: middle;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.room_header h2 {
|
||||
margin-top: 0px;
|
||||
margin-left: 75px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.room_avatar {
|
||||
width: 56px;
|
||||
line-height: 0px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.room_avatar img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.notif {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.historical_message .sender_avatar {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* spell out opacity and historical_message class names for Outlook aka Word */
|
||||
.historical_message .sender_name {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
.historical_message .message_time {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
.historical_message .message_body {
|
||||
color: #c7c7c7;
|
||||
}
|
||||
|
||||
.historical_message td,
|
||||
.message td {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sender_avatar {
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.sender_avatar img {
|
||||
margin-top: -2px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.sender_name {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
color: #a2a2a2;
|
||||
}
|
||||
|
||||
.message_time {
|
||||
text-align: right;
|
||||
width: 100px;
|
||||
font-size: 11px;
|
||||
color: #a2a2a2;
|
||||
}
|
||||
|
||||
.message_body {
|
||||
}
|
||||
|
||||
.notif_link td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notif_link a, .footer a {
|
||||
color: #454545;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.debug {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
45
res/templates-dinsic/notif.html
Normal file
45
res/templates-dinsic/notif.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% for message in notif.messages %}
|
||||
<tr class="{{ "historical_message" if message.is_historical else "message" }}">
|
||||
<td class="sender_avatar">
|
||||
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
|
||||
{% if message.sender_avatar_url %}
|
||||
<img alt="" class="sender_avatar" src="{{ message.sender_avatar_url|mxc_to_http(32,32) }}" />
|
||||
{% else %}
|
||||
{% if message.sender_hash % 3 == 0 %}
|
||||
<img class="sender_avatar" src="https://vector.im/beta/img/76cfa6.png" />
|
||||
{% elif message.sender_hash % 3 == 1 %}
|
||||
<img class="sender_avatar" src="https://vector.im/beta/img/50e2c2.png" />
|
||||
{% else %}
|
||||
<img class="sender_avatar" src="https://vector.im/beta/img/f4c371.png" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="message_contents">
|
||||
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
|
||||
<div class="sender_name">{% if message.msgtype == "m.emote" %}*{% endif %} {{ message.sender_name }}</div>
|
||||
{% endif %}
|
||||
<div class="message_body">
|
||||
{% if message.msgtype == "m.text" %}
|
||||
{{ message.body_text_html }}
|
||||
{% elif message.msgtype == "m.emote" %}
|
||||
{{ message.body_text_html }}
|
||||
{% elif message.msgtype == "m.notice" %}
|
||||
{{ message.body_text_html }}
|
||||
{% elif message.msgtype == "m.image" %}
|
||||
<img src="{{ message.image_url|mxc_to_http(640, 480, scale) }}" />
|
||||
{% elif message.msgtype == "m.file" %}
|
||||
<span class="filename">{{ message.body_text_plain }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="message_time">{{ message.ts|format_ts("%H:%M") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="notif_link">
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{ notif.link }}">Voir {{ room.title }}</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
16
res/templates-dinsic/notif.txt
Normal file
16
res/templates-dinsic/notif.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
{% for message in notif.messages %}
|
||||
{% if message.msgtype == "m.emote" %}* {% endif %}{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }})
|
||||
{% if message.msgtype == "m.text" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.emote" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.notice" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.image" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% elif message.msgtype == "m.file" %}
|
||||
{{ message.body_text_plain }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Voir {{ room.title }} à {{ notif.link }}
|
||||
55
res/templates-dinsic/notif_mail.html
Normal file
55
res/templates-dinsic/notif_mail.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style type="text/css">
|
||||
{% include 'mail.css' without context %}
|
||||
{% include "mail-%s.css" % app_name ignore missing without context %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table id="page">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td id="inner">
|
||||
<table class="header">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="salutation">Bonjour {{ user_display_name }},</div>
|
||||
<div class="summarytext">{{ summary_text }}</div>
|
||||
</td>
|
||||
<td class="logo">
|
||||
{% if app_name == "Riot" %}
|
||||
<img src="http://matrix.org/img/riot-logo-email.png" width="83" height="83" alt="[Riot]"/>
|
||||
{% elif app_name == "Vector" %}
|
||||
<img src="http://matrix.org/img/vector-logo-email.png" width="64" height="83" alt="[Vector]"/>
|
||||
{% else %}
|
||||
<img src="http://matrix.org/img/matrix-120x51.png" width="120" height="51" alt="[matrix]"/>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% for room in rooms %}
|
||||
{% include 'room.html' with context %}
|
||||
{% endfor %}
|
||||
<div class="footer">
|
||||
<a href="{{ unsubscribe_link }}">Se désinscrire</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="debug">
|
||||
Sending email at {{ reason.now|format_ts("%c") }} due to activity in room {{ reason.room_name }} because
|
||||
an event was received at {{ reason.received_at|format_ts("%c") }}
|
||||
which is more than {{ "%.1f"|format(reason.delay_before_mail_ms / (60*1000)) }} ({{ reason.delay_before_mail_ms }}) mins ago,
|
||||
{% if reason.last_sent_ts %}
|
||||
and the last time we sent a mail for this room was {{ reason.last_sent_ts|format_ts("%c") }},
|
||||
which is more than {{ "%.1f"|format(reason.throttle_ms / (60*1000)) }} (current throttle_ms) mins ago.
|
||||
{% else %}
|
||||
and we don't have a last time we sent a mail for this room.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
10
res/templates-dinsic/notif_mail.txt
Normal file
10
res/templates-dinsic/notif_mail.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Bonjour {{ user_display_name }},
|
||||
|
||||
{{ summary_text }}
|
||||
|
||||
{% for room in rooms %}
|
||||
{% include 'room.txt' with context %}
|
||||
{% endfor %}
|
||||
|
||||
Vous pouvez désactiver ces notifications en cliquant ici {{ unsubscribe_link }}
|
||||
|
||||
33
res/templates-dinsic/room.html
Normal file
33
res/templates-dinsic/room.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<table class="room">
|
||||
<tr class="room_header">
|
||||
<td class="room_avatar">
|
||||
{% if room.avatar_url %}
|
||||
<img alt="" src="{{ room.avatar_url|mxc_to_http(48,48) }}" />
|
||||
{% else %}
|
||||
{% if room.hash % 3 == 0 %}
|
||||
<img alt="" src="https://vector.im/beta/img/76cfa6.png" />
|
||||
{% elif room.hash % 3 == 1 %}
|
||||
<img alt="" src="https://vector.im/beta/img/50e2c2.png" />
|
||||
{% else %}
|
||||
<img alt="" src="https://vector.im/beta/img/f4c371.png" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="room_name" colspan="2">
|
||||
{{ room.title }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if room.invite %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{ room.link }}">Rejoindre la conversation.</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for notif in room.notifs %}
|
||||
{% include 'notif.html' with context %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
9
res/templates-dinsic/room.txt
Normal file
9
res/templates-dinsic/room.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ room.title }}
|
||||
|
||||
{% if room.invite %}
|
||||
Vous avez été invité, rejoignez la conversation en cliquant sur le lien suivant {{ room.link }}
|
||||
{% else %}
|
||||
{% for notif in room.notifs %}
|
||||
{% include 'notif.txt' with context %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -27,4 +27,4 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__version__ = "1.0.0rc3"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -184,11 +184,22 @@ class Auth(object):
|
||||
return event_auth.get_public_keys(invite_event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_by_req(self, request, allow_guest=False, rights="access"):
|
||||
def get_user_by_req(
|
||||
self,
|
||||
request,
|
||||
allow_guest=False,
|
||||
rights="access",
|
||||
allow_expired=False,
|
||||
):
|
||||
""" Get a registered user's ID.
|
||||
|
||||
Args:
|
||||
request - An HTTP request with an access_token query parameter.
|
||||
allow_expired - Whether to allow the request through even if the account is
|
||||
expired. If true, Synapse will still require an access token to be
|
||||
provided but won't check if the account it belongs to has expired. This
|
||||
works thanks to /login delivering access tokens regardless of accounts'
|
||||
expiration.
|
||||
Returns:
|
||||
defer.Deferred: resolves to a ``synapse.types.Requester`` object
|
||||
Raises:
|
||||
@@ -207,6 +218,7 @@ class Auth(object):
|
||||
)
|
||||
|
||||
user_id, app_service = yield self._get_appservice_user_id(request)
|
||||
|
||||
if user_id:
|
||||
request.authenticated_entity = user_id
|
||||
|
||||
@@ -229,7 +241,7 @@ class Auth(object):
|
||||
is_guest = user_info["is_guest"]
|
||||
|
||||
# Deny the request if the user account has expired.
|
||||
if self._account_validity.enabled:
|
||||
if self._account_validity.enabled and not allow_expired:
|
||||
user_id = user.to_string()
|
||||
expiration_ts = yield self.store.get_expiration_ts_for_user(user_id)
|
||||
if expiration_ts is not None and self.clock.time_msec() >= expiration_ts:
|
||||
@@ -268,39 +280,40 @@ class Auth(object):
|
||||
errcode=Codes.MISSING_TOKEN
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_appservice_user_id(self, request):
|
||||
app_service = self.store.get_app_service_by_token(
|
||||
self.get_access_token_from_request(
|
||||
request, self.TOKEN_NOT_FOUND_HTTP_STATUS
|
||||
)
|
||||
)
|
||||
|
||||
if app_service is None:
|
||||
defer.returnValue((None, None))
|
||||
return(None, None)
|
||||
|
||||
if app_service.ip_range_whitelist:
|
||||
ip_address = IPAddress(self.hs.get_ip_from_request(request))
|
||||
if ip_address not in app_service.ip_range_whitelist:
|
||||
defer.returnValue((None, None))
|
||||
return(None, None)
|
||||
|
||||
if b"user_id" not in request.args:
|
||||
defer.returnValue((app_service.sender, app_service))
|
||||
return(app_service.sender, app_service)
|
||||
|
||||
user_id = request.args[b"user_id"][0].decode('utf8')
|
||||
if app_service.sender == user_id:
|
||||
defer.returnValue((app_service.sender, app_service))
|
||||
return(app_service.sender, app_service)
|
||||
|
||||
if not app_service.is_interested_in_user(user_id):
|
||||
raise AuthError(
|
||||
403,
|
||||
"Application service cannot masquerade as this user."
|
||||
)
|
||||
if not (yield self.store.get_user_by_id(user_id)):
|
||||
raise AuthError(
|
||||
403,
|
||||
"Application service has not registered this user"
|
||||
)
|
||||
defer.returnValue((user_id, app_service))
|
||||
# Let ASes manipulate nonexistent users (e.g. to shadow-register them)
|
||||
# if not (yield self.store.get_user_by_id(user_id)):
|
||||
# raise AuthError(
|
||||
# 403,
|
||||
# "Application service has not registered this user"
|
||||
# )
|
||||
return(user_id, app_service)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_by_access_token(self, token, rights="access"):
|
||||
@@ -533,24 +546,15 @@ class Auth(object):
|
||||
defer.returnValue(user_info)
|
||||
|
||||
def get_appservice_by_req(self, request):
|
||||
try:
|
||||
token = self.get_access_token_from_request(
|
||||
request, self.TOKEN_NOT_FOUND_HTTP_STATUS
|
||||
)
|
||||
service = self.store.get_app_service_by_token(token)
|
||||
if not service:
|
||||
logger.warn("Unrecognised appservice access token.")
|
||||
(user_id, app_service) = self._get_appservice_user_id(request)
|
||||
if not app_service:
|
||||
raise AuthError(
|
||||
self.TOKEN_NOT_FOUND_HTTP_STATUS,
|
||||
"Unrecognised access token.",
|
||||
errcode=Codes.UNKNOWN_TOKEN
|
||||
)
|
||||
request.authenticated_entity = service.sender
|
||||
return defer.succeed(service)
|
||||
except KeyError:
|
||||
raise AuthError(
|
||||
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token."
|
||||
errcode=Codes.UNKNOWN_TOKEN,
|
||||
)
|
||||
request.authenticated_entity = app_service.sender
|
||||
return app_service
|
||||
|
||||
def is_server_admin(self, user):
|
||||
""" Check if the given user is a local server admin.
|
||||
|
||||
@@ -83,6 +83,7 @@ class EventTypes(object):
|
||||
RoomAvatar = "m.room.avatar"
|
||||
RoomEncryption = "m.room.encryption"
|
||||
GuestAccess = "m.room.guest_access"
|
||||
Encryption = "m.room.encryption"
|
||||
|
||||
# These are used for validation
|
||||
Message = "m.room.message"
|
||||
@@ -92,6 +93,8 @@ class EventTypes(object):
|
||||
ServerACL = "m.room.server_acl"
|
||||
Pinned = "m.room.pinned_events"
|
||||
|
||||
Retention = "m.room.retention"
|
||||
|
||||
|
||||
class RejectedReason(object):
|
||||
AUTH_ERROR = "auth_error"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -61,6 +62,13 @@ class Codes(object):
|
||||
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
|
||||
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
|
||||
PASSWORD_TOO_SHORT = "M_PASSWORD_TOO_SHORT"
|
||||
PASSWORD_NO_DIGIT = "M_PASSWORD_NO_DIGIT"
|
||||
PASSWORD_NO_UPPERCASE = "M_PASSWORD_NO_UPPERCASE"
|
||||
PASSWORD_NO_LOWERCASE = "M_PASSWORD_NO_LOWERCASE"
|
||||
PASSWORD_NO_SYMBOL = "M_PASSWORD_NO_SYMBOL"
|
||||
PASSWORD_IN_DICTIONARY = "M_PASSWORD_IN_DICTIONARY"
|
||||
WEAK_PASSWORD = "M_WEAK_PASSWORD"
|
||||
|
||||
|
||||
class CodeMessageException(RuntimeError):
|
||||
@@ -372,6 +380,22 @@ class IncompatibleRoomVersionError(SynapseError):
|
||||
)
|
||||
|
||||
|
||||
class PasswordRefusedError(SynapseError):
|
||||
"""A password has been refused, either during password reset/change or registration.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
msg="This password doesn't comply with the server's policy",
|
||||
errcode=Codes.WEAK_PASSWORD,
|
||||
):
|
||||
super(PasswordRefusedError, self).__init__(
|
||||
code=400,
|
||||
msg=msg,
|
||||
errcode=errcode,
|
||||
)
|
||||
|
||||
|
||||
class RequestSendFailed(RuntimeError):
|
||||
"""Sending a HTTP request over federation failed due to not being able to
|
||||
talk to the remote server for some reason.
|
||||
|
||||
@@ -176,7 +176,6 @@ class SynapseHomeServer(HomeServer):
|
||||
|
||||
resources.update({
|
||||
"/_matrix/client/api/v1": client_resource,
|
||||
"/_synapse/password_reset": client_resource,
|
||||
"/_matrix/client/r0": client_resource,
|
||||
"/_matrix/client/unstable": client_resource,
|
||||
"/_matrix/client/v2_alpha": client_resource,
|
||||
|
||||
@@ -265,7 +265,7 @@ class ApplicationService(object):
|
||||
def is_exclusive_room(self, room_id):
|
||||
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
|
||||
|
||||
def get_exlusive_user_regexes(self):
|
||||
def get_exclusive_user_regexes(self):
|
||||
"""Get the list of regexes used to determine if a user is exclusively
|
||||
registered by the AS
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2015-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +30,10 @@ class PasswordConfig(Config):
|
||||
self.password_enabled = password_config.get("enabled", True)
|
||||
self.password_pepper = password_config.get("pepper", "")
|
||||
|
||||
# Password policy
|
||||
self.password_policy = password_config.get("policy", {})
|
||||
self.password_policy_enabled = self.password_policy.pop("enabled", False)
|
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """\
|
||||
password_config:
|
||||
@@ -39,4 +45,34 @@ class PasswordConfig(Config):
|
||||
# DO NOT CHANGE THIS AFTER INITIAL SETUP!
|
||||
#
|
||||
#pepper: "EVEN_MORE_SECRET"
|
||||
|
||||
# Define and enforce a password policy. Each parameter is optional, boolean
|
||||
# parameters default to 'false' and integer parameters default to 0.
|
||||
# This is an early implementation of MSC2000.
|
||||
#
|
||||
#policy:
|
||||
# Whether to enforce the password policy.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# Minimum accepted length for a password.
|
||||
#
|
||||
#minimum_length: 15
|
||||
|
||||
# Whether a password must contain at least one digit.
|
||||
#
|
||||
#require_digit: true
|
||||
|
||||
# Whether a password must contain at least one symbol.
|
||||
# A symbol is any character that's not a number or a letter.
|
||||
#
|
||||
#require_symbol: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_lowercase: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_uppercase: true
|
||||
"""
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
from distutils.util import strtobool
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from synapse.config._base import Config, ConfigError
|
||||
from synapse.types import RoomAlias
|
||||
from synapse.util.stringutils import random_string_with_symbols
|
||||
@@ -41,9 +44,37 @@ class AccountValidityConfig(Config):
|
||||
|
||||
self.startup_job_max_delta = self.period * 10. / 100.
|
||||
|
||||
if self.renew_by_email_enabled and "public_baseurl" not in synapse_config:
|
||||
if self.renew_by_email_enabled:
|
||||
if "public_baseurl" not in synapse_config:
|
||||
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
|
||||
|
||||
template_dir = config.get("template_dir")
|
||||
|
||||
if not template_dir:
|
||||
template_dir = pkg_resources.resource_filename("synapse", "res/templates")
|
||||
|
||||
if "account_renewed_html_path" in config:
|
||||
file_path = os.path.join(template_dir, config["account_renewed_html_path"])
|
||||
|
||||
self.account_renewed_html_content = self.read_file(
|
||||
file_path, "account_validity.account_renewed_html_path"
|
||||
)
|
||||
else:
|
||||
self.account_renewed_html_content = (
|
||||
"<html><body>Your account has been successfully renewed.</body><html>"
|
||||
)
|
||||
|
||||
if "invalid_token_html_path" in config:
|
||||
file_path = os.path.join(template_dir, config["invalid_token_html_path"])
|
||||
|
||||
self.invalid_token_html_content = self.read_file(
|
||||
file_path, "account_validity.invalid_token_html_path"
|
||||
)
|
||||
else:
|
||||
self.invalid_token_html_content = (
|
||||
"<html><body>Invalid renewal token.</body><html>"
|
||||
)
|
||||
|
||||
|
||||
class RegistrationConfig(Config):
|
||||
|
||||
@@ -62,8 +93,19 @@ class RegistrationConfig(Config):
|
||||
|
||||
self.registrations_require_3pid = config.get("registrations_require_3pid", [])
|
||||
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
|
||||
self.check_is_for_allowed_local_3pids = config.get(
|
||||
"check_is_for_allowed_local_3pids", None
|
||||
)
|
||||
self.allow_invited_3pids = config.get("allow_invited_3pids", False)
|
||||
|
||||
self.disable_3pid_changes = config.get("disable_3pid_changes", False)
|
||||
|
||||
self.enable_3pid_lookup = config.get("enable_3pid_lookup", True)
|
||||
self.registration_shared_secret = config.get("registration_shared_secret")
|
||||
self.register_mxid_from_3pid = config.get("register_mxid_from_3pid")
|
||||
self.register_just_use_email_for_display_name = config.get(
|
||||
"register_just_use_email_for_display_name", False,
|
||||
)
|
||||
|
||||
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
|
||||
self.trusted_third_party_id_servers = config.get(
|
||||
@@ -83,6 +125,16 @@ class RegistrationConfig(Config):
|
||||
raise ConfigError('Invalid auto_join_rooms entry %s' % (room_alias,))
|
||||
self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True)
|
||||
|
||||
self.disable_set_displayname = config.get("disable_set_displayname", False)
|
||||
self.disable_set_avatar_url = config.get("disable_set_avatar_url", False)
|
||||
|
||||
self.replicate_user_profiles_to = config.get("replicate_user_profiles_to", [])
|
||||
if not isinstance(self.replicate_user_profiles_to, list):
|
||||
self.replicate_user_profiles_to = [self.replicate_user_profiles_to, ]
|
||||
|
||||
self.shadow_server = config.get("shadow_server", None)
|
||||
self.rewrite_identity_server_urls = config.get("rewrite_identity_server_urls", {})
|
||||
|
||||
self.disable_msisdn_registration = (
|
||||
config.get("disable_msisdn_registration", False)
|
||||
)
|
||||
@@ -140,6 +192,16 @@ class RegistrationConfig(Config):
|
||||
# period: 6w
|
||||
# renew_at: 1w
|
||||
# renew_email_subject: "Renew your %%(app)s account"
|
||||
# # Directory in which Synapse will try to find the HTML files to serve to the
|
||||
# # user when trying to renew an account. Optional, defaults to
|
||||
# # synapse/res/templates.
|
||||
# template_dir: "res/templates"
|
||||
# # HTML to be displayed to the user after they successfully renewed their
|
||||
# # account. Optional.
|
||||
# account_renewed_html_path: "account_renewed.html"
|
||||
# # HTML to be displayed when the user tries to renew an account with an invalid
|
||||
# # renewal token. Optional.
|
||||
# invalid_token_html_path: "invalid_token.html"
|
||||
|
||||
# The user must provide all of the below types of 3PID when registering.
|
||||
#
|
||||
@@ -152,9 +214,32 @@ class RegistrationConfig(Config):
|
||||
#
|
||||
#disable_msisdn_registration: true
|
||||
|
||||
# Derive the user's matrix ID from a type of 3PID used when registering.
|
||||
# This overrides any matrix ID the user proposes when calling /register
|
||||
# The 3PID type should be present in registrations_require_3pid to avoid
|
||||
# users failing to register if they don't specify the right kind of 3pid.
|
||||
#
|
||||
#register_mxid_from_3pid: email
|
||||
|
||||
# Uncomment to set the display name of new users to their email address,
|
||||
# rather than using the default heuristic.
|
||||
#
|
||||
#register_just_use_email_for_display_name: true
|
||||
|
||||
# Mandate that users are only allowed to associate certain formats of
|
||||
# 3PIDs with accounts on this server.
|
||||
#
|
||||
# Use an Identity Server to establish which 3PIDs are allowed to register?
|
||||
# Overrides allowed_local_3pids below.
|
||||
#
|
||||
#check_is_for_allowed_local_3pids: matrix.org
|
||||
#
|
||||
# If you are using an IS you can also check whether that IS registers
|
||||
# pending invites for the given 3PID (and then allow it to sign up on
|
||||
# the platform):
|
||||
#
|
||||
#allow_invited_3pids: False
|
||||
#
|
||||
#allowed_local_3pids:
|
||||
# - medium: email
|
||||
# pattern: '.*@matrix\\.org'
|
||||
@@ -163,6 +248,11 @@ class RegistrationConfig(Config):
|
||||
# - medium: msisdn
|
||||
# pattern: '\\+44'
|
||||
|
||||
# If true, stop users from trying to change the 3PIDs associated with
|
||||
# their accounts.
|
||||
#
|
||||
#disable_3pid_changes: False
|
||||
|
||||
# Enable 3PIDs lookup requests to identity servers from this server.
|
||||
#
|
||||
#enable_3pid_lookup: true
|
||||
@@ -204,6 +294,30 @@ class RegistrationConfig(Config):
|
||||
# - matrix.org
|
||||
# - vector.im
|
||||
|
||||
# If enabled, user IDs, display names and avatar URLs will be replicated
|
||||
# to this server whenever they change.
|
||||
# This is an experimental API currently implemented by sydent to support
|
||||
# cross-homeserver user directories.
|
||||
#
|
||||
#replicate_user_profiles_to: example.com
|
||||
|
||||
# If specified, attempt to replay registrations, profile changes & 3pid
|
||||
# bindings on the given target homeserver via the AS API. The HS is authed
|
||||
# via a given AS token.
|
||||
#
|
||||
#shadow_server:
|
||||
# hs_url: https://shadow.example.com
|
||||
# hs: shadow.example.com
|
||||
# as_token: 12u394refgbdhivsia
|
||||
|
||||
# If enabled, don't let users set their own display names/avatars
|
||||
# other than for the very first time (unless they are a server admin).
|
||||
# Useful when provisioning users based on the contents of a 3rd party
|
||||
# directory and to avoid ambiguities.
|
||||
#
|
||||
#disable_set_displayname: False
|
||||
#disable_set_avatar_url: False
|
||||
|
||||
# Users who register on this homeserver will automatically be joined
|
||||
# to these rooms
|
||||
#
|
||||
|
||||
@@ -84,11 +84,31 @@ class ServerConfig(Config):
|
||||
"require_auth_for_profile_requests", False,
|
||||
)
|
||||
|
||||
# If set to 'True', requires authentication to access the server's
|
||||
# public rooms directory through the client API, and forbids any other
|
||||
# homeserver to fetch it via federation.
|
||||
self.restrict_public_rooms_to_local_users = config.get(
|
||||
"restrict_public_rooms_to_local_users", False,
|
||||
if "restrict_public_rooms_to_local_users" in config and (
|
||||
"allow_public_rooms_without_auth" in config
|
||||
or "allow_public_rooms_over_federation" in config
|
||||
):
|
||||
raise ConfigError(
|
||||
"Can't use 'restrict_public_rooms_to_local_users' if"
|
||||
" 'allow_public_rooms_without_auth' and/or"
|
||||
" 'allow_public_rooms_over_federation' is set."
|
||||
)
|
||||
|
||||
# Check if the legacy "restrict_public_rooms_to_local_users" flag is set. This
|
||||
# flag is now obsolete but we need to check it for backward-compatibility.
|
||||
if config.get("restrict_public_rooms_to_local_users", False):
|
||||
self.allow_public_rooms_without_auth = False
|
||||
self.allow_public_rooms_over_federation = False
|
||||
else:
|
||||
# If set to 'False', requires authentication to access the server's public
|
||||
# rooms directory through the client API. Defaults to 'True'.
|
||||
self.allow_public_rooms_without_auth = config.get(
|
||||
"allow_public_rooms_without_auth", True
|
||||
)
|
||||
# If set to 'False', forbids any other homeserver to fetch the server's public
|
||||
# rooms directory via federation. Defaults to 'True'.
|
||||
self.allow_public_rooms_over_federation = config.get(
|
||||
"allow_public_rooms_over_federation", True
|
||||
)
|
||||
|
||||
default_room_version = config.get(
|
||||
@@ -202,6 +222,121 @@ class ServerConfig(Config):
|
||||
# events with profile information that differ from the target's global profile.
|
||||
self.allow_per_room_profiles = config.get("allow_per_room_profiles", True)
|
||||
|
||||
# Whether to show the users on this homeserver in the user directory. Defaults to
|
||||
# True.
|
||||
self.show_users_in_user_directory = config.get(
|
||||
"show_users_in_user_directory", True,
|
||||
)
|
||||
|
||||
retention_config = config.get("retention")
|
||||
if retention_config is None:
|
||||
retention_config = {}
|
||||
|
||||
self.retention_enabled = retention_config.get("enabled", False)
|
||||
|
||||
retention_default_policy = retention_config.get("default_policy")
|
||||
|
||||
if retention_default_policy is not None:
|
||||
self.retention_default_min_lifetime = retention_default_policy.get(
|
||||
"min_lifetime"
|
||||
)
|
||||
if self.retention_default_min_lifetime is not None:
|
||||
self.retention_default_min_lifetime = self.parse_duration(
|
||||
self.retention_default_min_lifetime
|
||||
)
|
||||
|
||||
self.retention_default_max_lifetime = retention_default_policy.get(
|
||||
"max_lifetime"
|
||||
)
|
||||
if self.retention_default_max_lifetime is not None:
|
||||
self.retention_default_max_lifetime = self.parse_duration(
|
||||
self.retention_default_max_lifetime
|
||||
)
|
||||
|
||||
if (
|
||||
self.retention_default_min_lifetime is not None
|
||||
and self.retention_default_max_lifetime is not None
|
||||
and (
|
||||
self.retention_default_min_lifetime
|
||||
> self.retention_default_max_lifetime
|
||||
)
|
||||
):
|
||||
raise ConfigError(
|
||||
"The default retention policy's 'min_lifetime' can not be greater"
|
||||
" than its 'max_lifetime'"
|
||||
)
|
||||
else:
|
||||
self.retention_default_min_lifetime = None
|
||||
self.retention_default_max_lifetime = None
|
||||
|
||||
self.retention_allowed_lifetime_min = retention_config.get("allowed_lifetime_min")
|
||||
if self.retention_allowed_lifetime_min is not None:
|
||||
self.retention_allowed_lifetime_min = self.parse_duration(
|
||||
self.retention_allowed_lifetime_min
|
||||
)
|
||||
|
||||
self.retention_allowed_lifetime_max = retention_config.get("allowed_lifetime_max")
|
||||
if self.retention_allowed_lifetime_max is not None:
|
||||
self.retention_allowed_lifetime_max = self.parse_duration(
|
||||
self.retention_allowed_lifetime_max
|
||||
)
|
||||
|
||||
if (
|
||||
self.retention_allowed_lifetime_min is not None
|
||||
and self.retention_allowed_lifetime_max is not None
|
||||
and self.retention_allowed_lifetime_min > self.retention_allowed_lifetime_max
|
||||
):
|
||||
raise ConfigError(
|
||||
"Invalid retention policy limits: 'allowed_lifetime_min' can not be"
|
||||
" greater than 'allowed_lifetime_max'"
|
||||
)
|
||||
|
||||
self.retention_purge_jobs = []
|
||||
for purge_job_config in retention_config.get("purge_jobs", []):
|
||||
interval_config = purge_job_config.get("interval")
|
||||
|
||||
if interval_config is None:
|
||||
raise ConfigError(
|
||||
"A retention policy's purge jobs configuration must have the"
|
||||
" 'interval' key set."
|
||||
)
|
||||
|
||||
interval = self.parse_duration(interval_config)
|
||||
|
||||
shortest_max_lifetime = purge_job_config.get("shortest_max_lifetime")
|
||||
|
||||
if shortest_max_lifetime is not None:
|
||||
shortest_max_lifetime = self.parse_duration(shortest_max_lifetime)
|
||||
|
||||
longest_max_lifetime = purge_job_config.get("longest_max_lifetime")
|
||||
|
||||
if longest_max_lifetime is not None:
|
||||
longest_max_lifetime = self.parse_duration(longest_max_lifetime)
|
||||
|
||||
if (
|
||||
shortest_max_lifetime is not None
|
||||
and longest_max_lifetime is not None
|
||||
and shortest_max_lifetime > longest_max_lifetime
|
||||
):
|
||||
raise ConfigError(
|
||||
"A retention policy's purge jobs configuration's"
|
||||
" 'shortest_max_lifetime' value can not be greater than its"
|
||||
" 'longest_max_lifetime' value."
|
||||
)
|
||||
|
||||
self.retention_purge_jobs.append({
|
||||
"interval": interval,
|
||||
"shortest_max_lifetime": shortest_max_lifetime,
|
||||
"longest_max_lifetime": longest_max_lifetime,
|
||||
})
|
||||
|
||||
if not self.retention_purge_jobs:
|
||||
self.retention_purge_jobs = [{
|
||||
"interval": self.parse_duration("1d"),
|
||||
"shortest_max_lifetime": None,
|
||||
"longest_max_lifetime": None,
|
||||
}]
|
||||
|
||||
self.listeners = []
|
||||
for listener in config.get("listeners", []):
|
||||
if not isinstance(listener.get("port", None), int):
|
||||
@@ -401,11 +536,15 @@ class ServerConfig(Config):
|
||||
#
|
||||
#require_auth_for_profile_requests: true
|
||||
|
||||
# If set to 'true', requires authentication to access the server's
|
||||
# public rooms directory through the client API, and forbids any other
|
||||
# homeserver to fetch it via federation. Defaults to 'false'.
|
||||
# If set to 'false', requires authentication to access the server's public rooms
|
||||
# directory through the client API. Defaults to 'true'.
|
||||
#
|
||||
#restrict_public_rooms_to_local_users: true
|
||||
#allow_public_rooms_without_auth: false
|
||||
|
||||
# If set to 'false', forbids any other homeserver to fetch the server's public
|
||||
# rooms directory via federation. Defaults to 'true'.
|
||||
#
|
||||
#allow_public_rooms_over_federation: false
|
||||
|
||||
# The default room version for newly created rooms.
|
||||
#
|
||||
@@ -631,6 +770,74 @@ class ServerConfig(Config):
|
||||
# Defaults to 'true'.
|
||||
#
|
||||
#allow_per_room_profiles: false
|
||||
|
||||
# Whether to show the users on this homeserver in the user directory. Defaults to
|
||||
# 'true'.
|
||||
#
|
||||
#show_users_in_user_directory: false
|
||||
|
||||
# Message retention policy at the server level.
|
||||
#
|
||||
# Room admins and mods can define a retention period for their rooms using the
|
||||
# 'm.room.retention' state event, and server admins can cap this period by setting
|
||||
# the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options.
|
||||
#
|
||||
# If this feature is enabled, Synapse will regularly look for and purge events
|
||||
# which are older than the room's maximum retention period. Synapse will also
|
||||
# filter events received over federation so that events that should have been
|
||||
# purged are ignored and not stored again.
|
||||
#
|
||||
retention:
|
||||
# The message retention policies feature is disabled by default. Uncomment the
|
||||
# following line to enable it.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# Default retention policy. If set, Synapse will apply it to rooms that lack the
|
||||
# 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't
|
||||
# matter much because Synapse doesn't take it into account yet.
|
||||
#
|
||||
#default_policy:
|
||||
# min_lifetime: 1d
|
||||
# max_lifetime: 1y
|
||||
|
||||
# Retention policy limits. If set, a user won't be able to send a
|
||||
# 'm.room.retention' event which features a 'min_lifetime' or a 'max_lifetime'
|
||||
# that's not within this range. This is especially useful in closed federations,
|
||||
# in which server admins can make sure every federating server applies the same
|
||||
# rules.
|
||||
#
|
||||
#allowed_lifetime_min: 1d
|
||||
#allowed_lifetime_max: 1y
|
||||
|
||||
# Server admins can define the settings of the background jobs purging the
|
||||
# events which lifetime has expired under the 'purge_jobs' section.
|
||||
#
|
||||
# If no configuration is provided, a single job will be set up to delete expired
|
||||
# events in every room daily.
|
||||
#
|
||||
# Each job's configuration defines which range of message lifetimes the job
|
||||
# takes care of. For example, if 'shortest_max_lifetime' is '2d' and
|
||||
# 'longest_max_lifetime' is '3d', the job will handle purging expired events in
|
||||
# rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and
|
||||
# lower than or equal to 3 days. Both the minimum and the maximum value of a
|
||||
# range are optional, e.g. a job with no 'shortest_max_lifetime' and a
|
||||
# 'longest_max_lifetime' of '3d' will handle every room with a retention policy
|
||||
# which 'max_lifetime' is lower than or equal to three days.
|
||||
#
|
||||
# The rationale for this per-job configuration is that some rooms might have a
|
||||
# retention policy with a low 'max_lifetime', where history needs to be purged
|
||||
# of outdated messages on a very frequent basis (e.g. every 5min), but not want
|
||||
# that purge to be performed by a job that's iterating over every room it knows,
|
||||
# which would be quite heavy on the server.
|
||||
#
|
||||
#purge_jobs:
|
||||
# - shortest_max_lifetime: 1d
|
||||
# longest_max_lifetime: 3d
|
||||
# interval: 5m:
|
||||
# - shortest_max_lifetime: 3d
|
||||
# longest_max_lifetime: 1y
|
||||
# interval: 24h
|
||||
""" % locals()
|
||||
|
||||
def read_arguments(self, args):
|
||||
|
||||
@@ -24,6 +24,7 @@ class UserDirectoryConfig(Config):
|
||||
def read_config(self, config):
|
||||
self.user_directory_search_enabled = True
|
||||
self.user_directory_search_all_users = False
|
||||
self.user_directory_defer_to_id_server = None
|
||||
user_directory_config = config.get("user_directory", None)
|
||||
if user_directory_config:
|
||||
self.user_directory_search_enabled = (
|
||||
@@ -32,6 +33,9 @@ class UserDirectoryConfig(Config):
|
||||
self.user_directory_search_all_users = (
|
||||
user_directory_config.get("search_all_users", False)
|
||||
)
|
||||
self.user_directory_defer_to_id_server = (
|
||||
user_directory_config.get("defer_to_id_server", None)
|
||||
)
|
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """
|
||||
@@ -50,4 +54,9 @@ class UserDirectoryConfig(Config):
|
||||
#user_directory:
|
||||
# enabled: true
|
||||
# search_all_users: false
|
||||
#
|
||||
# # If this is set, user search will be delegated to this ID server instead
|
||||
# # of synapse performing the search itself.
|
||||
# # This is an experimental API.
|
||||
# defer_to_id_server: https://id.example.com
|
||||
"""
|
||||
|
||||
@@ -46,13 +46,26 @@ class SpamChecker(object):
|
||||
|
||||
return self.spam_checker.check_event_for_spam(event)
|
||||
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, room_id):
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
|
||||
room_id, new_room, published_room):
|
||||
"""Checks if a given user may send an invite
|
||||
|
||||
If this method returns false, the invite will be rejected.
|
||||
|
||||
Args:
|
||||
userid (string): The sender's user ID
|
||||
inviter_userid (str)
|
||||
invitee_userid (str|None): The user ID of the invitee. Is None
|
||||
if this is a third party invite and the 3PID is not bound to a
|
||||
user ID.
|
||||
third_party_invite (dict|None): If a third party invite then is a
|
||||
dict containing the medium and address of the invitee.
|
||||
room_id (str)
|
||||
new_room (bool): Whether the user is being invited to the room as
|
||||
part of a room creation, if so the invitee would have been
|
||||
included in the call to `user_may_create_room`.
|
||||
published_room (bool): Whether the room the user is being invited
|
||||
to has been published in the local homeserver's public room
|
||||
directory.
|
||||
|
||||
Returns:
|
||||
bool: True if the user may send an invite, otherwise False
|
||||
@@ -60,15 +73,25 @@ class SpamChecker(object):
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_invite(inviter_userid, invitee_userid, room_id)
|
||||
return self.spam_checker.user_may_invite(
|
||||
inviter_userid, invitee_userid, third_party_invite, room_id, new_room,
|
||||
published_room,
|
||||
)
|
||||
|
||||
def user_may_create_room(self, userid):
|
||||
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
|
||||
cloning):
|
||||
"""Checks if a given user may create a room
|
||||
|
||||
If this method returns false, the creation request will be rejected.
|
||||
|
||||
Args:
|
||||
userid (string): The sender's user ID
|
||||
invite_list (list[str]): List of user IDs that would be invited to
|
||||
the new room.
|
||||
third_party_invite_list (list[dict]): List of third party invites
|
||||
for the new room.
|
||||
cloning (bool): Whether the user is cloning an existing room, e.g.
|
||||
upgrading a room.
|
||||
|
||||
Returns:
|
||||
bool: True if the user may create a room, otherwise False
|
||||
@@ -76,7 +99,9 @@ class SpamChecker(object):
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_create_room(userid)
|
||||
return self.spam_checker.user_may_create_room(
|
||||
userid, invite_list, third_party_invite_list, cloning,
|
||||
)
|
||||
|
||||
def user_may_create_room_alias(self, userid, room_alias):
|
||||
"""Checks if a given user may create a room alias
|
||||
@@ -111,3 +136,21 @@ class SpamChecker(object):
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_publish_room(userid, room_id)
|
||||
|
||||
def user_may_join_room(self, userid, room_id, is_invited):
|
||||
"""Checks if a given users is allowed to join a room.
|
||||
|
||||
Is not called when the user creates a room.
|
||||
|
||||
Args:
|
||||
userid (str)
|
||||
room_id (str)
|
||||
is_invited (bool): Whether the user is invited into the room
|
||||
|
||||
Returns:
|
||||
bool: Whether the user may join the room
|
||||
"""
|
||||
if self.spam_checker is None:
|
||||
return True
|
||||
|
||||
return self.spam_checker.user_may_join_room(userid, room_id, is_invited)
|
||||
|
||||
@@ -17,8 +17,8 @@ from twisted.internet import defer
|
||||
|
||||
|
||||
class ThirdPartyEventRules(object):
|
||||
"""Allows server admins to provide a Python module implementing an extra set of rules
|
||||
to apply when processing events.
|
||||
"""Allows server admins to provide a Python module implementing an extra
|
||||
set of rules to apply when processing events.
|
||||
|
||||
This is designed to help admins of closed federations with enforcing custom
|
||||
behaviours.
|
||||
@@ -35,7 +35,10 @@ class ThirdPartyEventRules(object):
|
||||
module, config = hs.config.third_party_event_rules
|
||||
|
||||
if module is not None:
|
||||
self.third_party_rules = module(config=config)
|
||||
self.third_party_rules = module(
|
||||
config=config,
|
||||
http_client=hs.get_simple_http_client(),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_event_allowed(self, event, context):
|
||||
@@ -46,7 +49,7 @@ class ThirdPartyEventRules(object):
|
||||
context (synapse.events.snapshot.EventContext): The context of the event.
|
||||
|
||||
Returns:
|
||||
defer.Deferred(bool), True if the event should be allowed, False if not.
|
||||
defer.Deferred[bool]: True if the event should be allowed, False if not.
|
||||
"""
|
||||
if self.third_party_rules is None:
|
||||
defer.returnValue(True)
|
||||
@@ -60,3 +63,52 @@ class ThirdPartyEventRules(object):
|
||||
|
||||
ret = yield self.third_party_rules.check_event_allowed(event, state_events)
|
||||
defer.returnValue(ret)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_create_room(self, requester, config, is_requester_admin):
|
||||
"""Intercept requests to create room to allow, deny or update the
|
||||
request config.
|
||||
|
||||
Args:
|
||||
requester (Requester)
|
||||
config (dict): The creation config from the client.
|
||||
is_requester_admin (bool): If the requester is an admin
|
||||
|
||||
Returns:
|
||||
defer.Deferred
|
||||
"""
|
||||
|
||||
if self.third_party_rules is None:
|
||||
return
|
||||
|
||||
yield self.third_party_rules.on_create_room(
|
||||
requester, config, is_requester_admin
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_threepid_can_be_invited(self, medium, address, room_id):
|
||||
"""Check if a provided 3PID can be invited in the given room.
|
||||
|
||||
Args:
|
||||
medium (str): The 3PID's medium.
|
||||
address (str): The 3PID's address.
|
||||
room_id (str): The room we want to invite the threepid to.
|
||||
|
||||
Returns:
|
||||
defer.Deferred[bool], True if the 3PID can be invited, False if not.
|
||||
"""
|
||||
|
||||
if self.third_party_rules is None:
|
||||
defer.returnValue(True)
|
||||
|
||||
state_ids = yield self.store.get_filtered_current_state_ids(room_id)
|
||||
room_state_events = yield self.store.get_events(state_ids.values())
|
||||
|
||||
state_events = {}
|
||||
for key, event_id in state_ids.items():
|
||||
state_events[key] = room_state_events[event_id]
|
||||
|
||||
ret = yield self.third_party_rules.check_threepid_can_be_invited(
|
||||
medium, address, state_events,
|
||||
)
|
||||
defer.returnValue(ret)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from six import string_types
|
||||
from six import integer_types, string_types
|
||||
|
||||
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
@@ -22,11 +22,12 @@ from synapse.types import EventID, RoomID, UserID
|
||||
|
||||
|
||||
class EventValidator(object):
|
||||
def validate_new(self, event):
|
||||
def validate_new(self, event, config):
|
||||
"""Validates the event has roughly the right format
|
||||
|
||||
Args:
|
||||
event (FrozenEvent)
|
||||
event (FrozenEvent): The event to validate.
|
||||
config (Config): The homeserver's configuration.
|
||||
"""
|
||||
self.validate_builder(event)
|
||||
|
||||
@@ -67,6 +68,99 @@ class EventValidator(object):
|
||||
Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
if event.type == EventTypes.Retention:
|
||||
self._validate_retention(event, config)
|
||||
|
||||
def _validate_retention(self, event, config):
|
||||
"""Checks that an event that defines the retention policy for a room respects the
|
||||
boundaries imposed by the server's administrator.
|
||||
|
||||
Args:
|
||||
event (FrozenEvent): The event to validate.
|
||||
config (Config): The homeserver's configuration.
|
||||
"""
|
||||
min_lifetime = event.content.get("min_lifetime")
|
||||
max_lifetime = event.content.get("max_lifetime")
|
||||
|
||||
if min_lifetime is not None:
|
||||
if not isinstance(min_lifetime, integer_types):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg="'min_lifetime' must be an integer",
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
if (
|
||||
config.retention_allowed_lifetime_min is not None
|
||||
and min_lifetime < config.retention_allowed_lifetime_min
|
||||
):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=(
|
||||
"'min_lifetime' can't be lower than the minimum allowed"
|
||||
" value enforced by the server's administrator"
|
||||
),
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
if (
|
||||
config.retention_allowed_lifetime_max is not None
|
||||
and min_lifetime > config.retention_allowed_lifetime_max
|
||||
):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=(
|
||||
"'min_lifetime' can't be greater than the maximum allowed"
|
||||
" value enforced by the server's administrator"
|
||||
),
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
if max_lifetime is not None:
|
||||
if not isinstance(max_lifetime, integer_types):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg="'max_lifetime' must be an integer",
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
if (
|
||||
config.retention_allowed_lifetime_min is not None
|
||||
and max_lifetime < config.retention_allowed_lifetime_min
|
||||
):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=(
|
||||
"'max_lifetime' can't be lower than the minimum allowed value"
|
||||
" enforced by the server's administrator"
|
||||
),
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
if (
|
||||
config.retention_allowed_lifetime_max is not None
|
||||
and max_lifetime > config.retention_allowed_lifetime_max
|
||||
):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=(
|
||||
"'max_lifetime' can't be greater than the maximum allowed"
|
||||
" value enforced by the server's administrator"
|
||||
),
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
if (
|
||||
min_lifetime is not None
|
||||
and max_lifetime is not None
|
||||
and min_lifetime > max_lifetime
|
||||
):
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg="'min_lifetime' can't be greater than 'max_lifetime",
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
def validate_builder(self, event):
|
||||
"""Validates that the builder/event has roughly the right format. Only
|
||||
checks values that we expect a proto event to have, rather than all the
|
||||
|
||||
@@ -720,15 +720,15 @@ class PublicRoomList(BaseFederationServlet):
|
||||
|
||||
PATH = "/publicRooms"
|
||||
|
||||
def __init__(self, handler, authenticator, ratelimiter, server_name, deny_access):
|
||||
def __init__(self, handler, authenticator, ratelimiter, server_name, allow_access):
|
||||
super(PublicRoomList, self).__init__(
|
||||
handler, authenticator, ratelimiter, server_name,
|
||||
)
|
||||
self.deny_access = deny_access
|
||||
self.allow_access = allow_access
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, origin, content, query):
|
||||
if self.deny_access:
|
||||
if not self.allow_access:
|
||||
raise FederationDeniedError(origin)
|
||||
|
||||
limit = parse_integer_from_args(query, "limit", 0)
|
||||
@@ -1455,7 +1455,7 @@ def register_servlets(hs, resource, authenticator, ratelimiter, servlet_groups=N
|
||||
authenticator=authenticator,
|
||||
ratelimiter=ratelimiter,
|
||||
server_name=hs.hostname,
|
||||
deny_access=hs.config.restrict_public_rooms_to_local_users,
|
||||
allow_access=hs.config.allow_public_rooms_over_federation,
|
||||
).register(resource)
|
||||
|
||||
if "group_server" in servlet_groups:
|
||||
|
||||
@@ -110,6 +110,9 @@ class AccountValidityHandler(object):
|
||||
# Stop right here if the user doesn't have at least one email address.
|
||||
# In this case, they will have to ask their server admin to renew their
|
||||
# account manually.
|
||||
# We don't need to do a specific check to make sure the account isn't
|
||||
# deactivated, as a deactivated account isn't supposed to have any
|
||||
# email address attached to it.
|
||||
if not addresses:
|
||||
return
|
||||
|
||||
@@ -220,11 +223,19 @@ class AccountValidityHandler(object):
|
||||
|
||||
Args:
|
||||
renewal_token (str): Token sent with the renewal request.
|
||||
Returns:
|
||||
bool: Whether the provided token is valid.
|
||||
"""
|
||||
try:
|
||||
user_id = yield self.store.get_user_from_renewal_token(renewal_token)
|
||||
except StoreError:
|
||||
defer.returnValue(False)
|
||||
|
||||
logger.debug("Renewing an account for user %s", user_id)
|
||||
yield self.renew_account_for_user(user_id)
|
||||
|
||||
defer.returnValue(True)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def renew_account_for_user(self, user_id, expiration_ts=None, email_sent=False):
|
||||
"""Renews the account attached to a given user by pushing back the
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017, 2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -33,6 +34,7 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
self._device_handler = hs.get_device_handler()
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
self._identity_handler = hs.get_handlers().identity_handler
|
||||
self._profile_handler = hs.get_profile_handler()
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
|
||||
# Flag that indicates whether the process to part users from rooms is running
|
||||
@@ -42,6 +44,8 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
# it left off (if it has work left to do).
|
||||
hs.get_reactor().callWhenRunning(self._start_user_parting)
|
||||
|
||||
self._account_validity_enabled = hs.config.account_validity.enabled
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def deactivate_account(self, user_id, erase_data, id_server=None):
|
||||
"""Deactivate a user's account
|
||||
@@ -98,6 +102,9 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
|
||||
yield self.store.user_set_password_hash(user_id, None)
|
||||
|
||||
user = UserID.from_string(user_id)
|
||||
yield self._profile_handler.set_active(user, False, False)
|
||||
|
||||
# Add the user to a table of users pending deactivation (ie.
|
||||
# removal from all the rooms they're a member of)
|
||||
yield self.store.add_user_pending_deactivation(user_id)
|
||||
@@ -114,6 +121,13 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
# parts users from rooms (if it isn't already running)
|
||||
self._start_user_parting()
|
||||
|
||||
# Remove all information on the user from the account_validity table.
|
||||
if self._account_validity_enabled:
|
||||
yield self.store.delete_account_validity_for_user(user_id)
|
||||
|
||||
# Mark the user as deactivated.
|
||||
yield self.store.set_user_deactivated_status(user_id, True)
|
||||
|
||||
defer.returnValue(identity_server_supports_unbinding)
|
||||
|
||||
def _start_user_parting(self):
|
||||
|
||||
@@ -568,6 +568,12 @@ class DeviceListEduUpdater(object):
|
||||
stream_id = result["stream_id"]
|
||||
devices = result["devices"]
|
||||
|
||||
for device in devices:
|
||||
logger.debug(
|
||||
"Handling resync update %r/%r, ID: %r",
|
||||
user_id, device["device_id"], stream_id,
|
||||
)
|
||||
|
||||
# If the remote server has more than ~1000 devices for this user
|
||||
# we assume that something is going horribly wrong (e.g. a bot
|
||||
# that logs in and creates a new device every time it tries to
|
||||
|
||||
@@ -1363,8 +1363,12 @@ class FederationHandler(BaseHandler):
|
||||
if self.hs.config.block_non_admin_invites:
|
||||
raise SynapseError(403, "This server does not accept room invites")
|
||||
|
||||
is_published = yield self.store.is_room_published(event.room_id)
|
||||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
event.sender, event.state_key, event.room_id,
|
||||
event.sender, event.state_key, None,
|
||||
room_id=event.room_id, new_room=False,
|
||||
published_room=is_published,
|
||||
):
|
||||
raise SynapseError(
|
||||
403, "This user is not permitted to send invites to this server/user"
|
||||
@@ -2606,7 +2610,7 @@ class FederationHandler(BaseHandler):
|
||||
room_version, event_dict, event, context
|
||||
)
|
||||
|
||||
EventValidator().validate_new(event)
|
||||
EventValidator().validate_new(event, self.config)
|
||||
|
||||
# We need to tell the transaction queue to send this out, even
|
||||
# though the sender isn't a local user.
|
||||
@@ -2677,12 +2681,6 @@ class FederationHandler(BaseHandler):
|
||||
# though the sender isn't a local user.
|
||||
event.internal_metadata.send_on_behalf_of = get_domain_from_id(event.sender)
|
||||
|
||||
# XXX we send the invite here, but send_membership_event also sends it,
|
||||
# so we end up making two requests. I think this is redundant.
|
||||
returned_invite = yield self.send_invite(origin, event)
|
||||
# TODO: Make sure the signatures actually are correct.
|
||||
event.signatures.update(returned_invite.signatures)
|
||||
|
||||
member_handler = self.hs.get_room_member_handler()
|
||||
yield member_handler.send_membership_event(None, event, context)
|
||||
|
||||
@@ -2717,7 +2715,7 @@ class FederationHandler(BaseHandler):
|
||||
event, context = yield self.event_creation_handler.create_new_client_event(
|
||||
builder=builder,
|
||||
)
|
||||
EventValidator().validate_new(event)
|
||||
EventValidator().validate_new(event, self.config)
|
||||
defer.returnValue((event, context))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2018, 2019 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -20,13 +20,18 @@
|
||||
import logging
|
||||
|
||||
from canonicaljson import json
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
from signedjson.sign import verify_signed_json
|
||||
from unpaddedbase64 import decode_base64
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
CodeMessageException,
|
||||
Codes,
|
||||
HttpResponseException,
|
||||
ProxiedRequestError,
|
||||
SynapseError,
|
||||
)
|
||||
|
||||
@@ -47,6 +52,8 @@ class IdentityHandler(BaseHandler):
|
||||
self.trust_any_id_server_just_for_testing_do_not_use = (
|
||||
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
|
||||
)
|
||||
self.rewrite_identity_server_urls = hs.config.rewrite_identity_server_urls
|
||||
self._enable_lookup = hs.config.enable_3pid_lookup
|
||||
|
||||
def _should_trust_id_server(self, id_server):
|
||||
if id_server not in self.trusted_id_servers:
|
||||
@@ -84,7 +91,10 @@ class IdentityHandler(BaseHandler):
|
||||
'credentials', id_server
|
||||
)
|
||||
defer.returnValue(None)
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
try:
|
||||
data = yield self.http_client.get_json(
|
||||
"https://%s%s" % (
|
||||
@@ -120,10 +130,18 @@ class IdentityHandler(BaseHandler):
|
||||
else:
|
||||
raise SynapseError(400, "No client_secret in creds")
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now, but only for sending the request (not
|
||||
# storing in the database).
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server_host = self.rewrite_identity_server_urls[id_server]
|
||||
else:
|
||||
id_server_host = id_server
|
||||
|
||||
try:
|
||||
data = yield self.http_client.post_urlencoded_get_json(
|
||||
"https://%s%s" % (
|
||||
id_server, "/_matrix/identity/api/v1/3pid/bind"
|
||||
id_server_host, "/_matrix/identity/api/v1/3pid/bind"
|
||||
),
|
||||
{
|
||||
'sid': creds['sid'],
|
||||
@@ -221,6 +239,16 @@ class IdentityHandler(BaseHandler):
|
||||
b"Authorization": auth_headers,
|
||||
}
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
#
|
||||
# Note that destination_is has to be the real id_server, not
|
||||
# the server we connect to.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
|
||||
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
|
||||
|
||||
try:
|
||||
yield self.http_client.post_json_get_json(
|
||||
url,
|
||||
@@ -267,6 +295,11 @@ class IdentityHandler(BaseHandler):
|
||||
'send_attempt': send_attempt,
|
||||
}
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
|
||||
if next_link:
|
||||
params.update({'next_link': next_link})
|
||||
|
||||
@@ -301,7 +334,10 @@ class IdentityHandler(BaseHandler):
|
||||
'send_attempt': send_attempt,
|
||||
}
|
||||
params.update(kwargs)
|
||||
|
||||
# if we have a rewrite rule set for the identity server,
|
||||
# apply it now.
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
id_server = self.rewrite_identity_server_urls[id_server]
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
"https://%s%s" % (
|
||||
@@ -314,3 +350,125 @@ class IdentityHandler(BaseHandler):
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied requestToken failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def lookup_3pid(self, id_server, medium, address):
|
||||
"""Looks up a 3pid in the passed identity server.
|
||||
|
||||
Args:
|
||||
id_server (str): The server name (including port, if required)
|
||||
of the identity server to use.
|
||||
medium (str): The type of the third party identifier (e.g. "email").
|
||||
address (str): The third party identifier (e.g. "foo@example.com").
|
||||
|
||||
Returns:
|
||||
Deferred[dict]: The result of the lookup. See
|
||||
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
|
||||
for details
|
||||
"""
|
||||
if not self._should_trust_id_server(id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server,
|
||||
Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
|
||||
if not self._enable_lookup:
|
||||
raise AuthError(
|
||||
403, "Looking up third-party identifiers is denied from this server",
|
||||
)
|
||||
|
||||
target = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
try:
|
||||
data = yield self.http_client.get_json(
|
||||
"https://%s/_matrix/identity/api/v1/lookup" % (target,),
|
||||
{
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
}
|
||||
)
|
||||
|
||||
if "mxid" in data:
|
||||
if "signatures" not in data:
|
||||
raise AuthError(401, "No signatures on 3pid binding")
|
||||
yield self._verify_any_signature(data, id_server)
|
||||
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied lookup failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except IOError as e:
|
||||
logger.info("Failed to contact %r: %s", id_server, e)
|
||||
raise ProxiedRequestError(503, "Failed to contact identity server")
|
||||
|
||||
defer.returnValue(data)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def bulk_lookup_3pid(self, id_server, threepids):
|
||||
"""Looks up given 3pids in the passed identity server.
|
||||
|
||||
Args:
|
||||
id_server (str): The server name (including port, if required)
|
||||
of the identity server to use.
|
||||
threepids ([[str, str]]): The third party identifiers to lookup, as
|
||||
a list of 2-string sized lists ([medium, address]).
|
||||
|
||||
Returns:
|
||||
Deferred[dict]: The result of the lookup. See
|
||||
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
|
||||
for details
|
||||
"""
|
||||
if not self._should_trust_id_server(id_server):
|
||||
raise SynapseError(
|
||||
400, "Untrusted ID server '%s'" % id_server,
|
||||
Codes.SERVER_NOT_TRUSTED
|
||||
)
|
||||
|
||||
if not self._enable_lookup:
|
||||
raise AuthError(
|
||||
403, "Looking up third-party identifiers is denied from this server",
|
||||
)
|
||||
|
||||
target = self.rewrite_identity_server_urls.get(id_server, id_server)
|
||||
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
"https://%s/_matrix/identity/api/v1/bulk_lookup" % (target,),
|
||||
{
|
||||
"threepids": threepids,
|
||||
}
|
||||
)
|
||||
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied lookup failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except IOError as e:
|
||||
logger.info("Failed to contact %r: %s", id_server, e)
|
||||
raise ProxiedRequestError(503, "Failed to contact identity server")
|
||||
|
||||
defer.returnValue(data)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _verify_any_signature(self, data, server_hostname):
|
||||
if server_hostname not in data["signatures"]:
|
||||
raise AuthError(401, "No signature from server %s" % (server_hostname,))
|
||||
|
||||
for key_name, signature in data["signatures"][server_hostname].items():
|
||||
target = self.rewrite_identity_server_urls.get(
|
||||
server_hostname, server_hostname,
|
||||
)
|
||||
|
||||
key_data = yield self.http_client.get_json(
|
||||
"https://%s/_matrix/identity/api/v1/pubkey/%s" %
|
||||
(target, key_name,),
|
||||
)
|
||||
if "public_key" not in key_data:
|
||||
raise AuthError(401, "No public key named %s from %s" %
|
||||
(key_name, server_hostname,))
|
||||
verify_signed_json(
|
||||
data,
|
||||
server_hostname,
|
||||
decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
|
||||
)
|
||||
return
|
||||
|
||||
raise AuthError(401, "No signature from server %s" % (server_hostname,))
|
||||
|
||||
@@ -372,7 +372,7 @@ class EventCreationHandler(object):
|
||||
"You must be in the room to create an alias for it",
|
||||
)
|
||||
|
||||
self.validator.validate_new(event)
|
||||
self.validator.validate_new(event, self.config)
|
||||
|
||||
defer.returnValue((event, context))
|
||||
|
||||
@@ -604,7 +604,7 @@ class EventCreationHandler(object):
|
||||
if requester:
|
||||
context.app_service = requester.app_service
|
||||
|
||||
self.validator.validate_new(event)
|
||||
self.validator.validate_new(event, self.config)
|
||||
|
||||
# If this event is an annotation then we check that that the sender
|
||||
# can't annotate the same way twice (e.g. stops users from liking an
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
# limitations under the License.
|
||||
import logging
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.types import RoomStreamToken
|
||||
from synapse.util.async_helpers import ReadWriteLock
|
||||
@@ -79,6 +82,114 @@ class PaginationHandler(object):
|
||||
self._purges_by_id = {}
|
||||
self._event_serializer = hs.get_event_client_serializer()
|
||||
|
||||
self._retention_default_max_lifetime = hs.config.retention_default_max_lifetime
|
||||
|
||||
if hs.config.retention_enabled:
|
||||
# Run the purge jobs described in the configuration file.
|
||||
for job in hs.config.retention_purge_jobs:
|
||||
self.clock.looping_call(
|
||||
run_as_background_process,
|
||||
job["interval"],
|
||||
"purge_history_for_rooms_in_range",
|
||||
self.purge_history_for_rooms_in_range,
|
||||
job["shortest_max_lifetime"],
|
||||
job["longest_max_lifetime"],
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def purge_history_for_rooms_in_range(self, min_ms, max_ms):
|
||||
"""Purge outdated events from rooms within the given retention range.
|
||||
|
||||
If a default retention policy is defined in the server's configuration and its
|
||||
'max_lifetime' is within this range, also targets rooms which don't have a
|
||||
retention policy.
|
||||
|
||||
Args:
|
||||
min_ms (int|None): Duration in milliseconds that define the lower limit of
|
||||
the range to handle (exclusive). If None, it means that the range has no
|
||||
lower limit.
|
||||
max_ms (int|None): Duration in milliseconds that define the upper limit of
|
||||
the range to handle (inclusive). If None, it means that the range has no
|
||||
upper limit.
|
||||
"""
|
||||
# We want the storage layer to to include rooms with no retention policy in its
|
||||
# return value only if a default retention policy is defined in the server's
|
||||
# configuration and that policy's 'max_lifetime' is either lower (or equal) than
|
||||
# max_ms or higher than min_ms (or both).
|
||||
if self._retention_default_max_lifetime is not None:
|
||||
include_null = True
|
||||
|
||||
if min_ms is not None and min_ms >= self._retention_default_max_lifetime:
|
||||
# The default max_lifetime is lower than (or equal to) min_ms.
|
||||
include_null = False
|
||||
|
||||
if max_ms is not None and max_ms < self._retention_default_max_lifetime:
|
||||
# The default max_lifetime is higher than max_ms.
|
||||
include_null = False
|
||||
else:
|
||||
include_null = False
|
||||
|
||||
rooms = yield self.store.get_rooms_for_retention_period_in_range(
|
||||
min_ms, max_ms, include_null
|
||||
)
|
||||
|
||||
for room_id, retention_policy in iteritems(rooms):
|
||||
if room_id in self._purges_in_progress_by_room:
|
||||
logger.warning(
|
||||
"[purge] not purging room %s as there's an ongoing purge running"
|
||||
" for this room",
|
||||
room_id,
|
||||
)
|
||||
continue
|
||||
|
||||
max_lifetime = retention_policy["max_lifetime"]
|
||||
|
||||
if max_lifetime is None:
|
||||
# If max_lifetime is None, it means that include_null equals True,
|
||||
# therefore we can safely assume that there is a default policy defined
|
||||
# in the server's configuration.
|
||||
max_lifetime = self._retention_default_max_lifetime
|
||||
|
||||
# Figure out what token we should start purging at.
|
||||
ts = self.clock.time_msec() - max_lifetime
|
||||
|
||||
stream_ordering = (
|
||||
yield self.store.find_first_stream_ordering_after_ts(ts)
|
||||
)
|
||||
|
||||
r = (
|
||||
yield self.store.get_room_event_after_stream_ordering(
|
||||
room_id, stream_ordering,
|
||||
)
|
||||
)
|
||||
if not r:
|
||||
logger.warning(
|
||||
"[purge] purging events not possible: No event found "
|
||||
"(ts %i => stream_ordering %i)",
|
||||
ts, stream_ordering,
|
||||
)
|
||||
continue
|
||||
|
||||
(stream, topo, _event_id) = r
|
||||
token = "t%d-%d" % (topo, stream)
|
||||
|
||||
purge_id = random_string(16)
|
||||
|
||||
self._purges_by_id[purge_id] = PurgeStatus()
|
||||
|
||||
logger.info(
|
||||
"Starting purging events in room %s (purge_id %s)" % (room_id, purge_id)
|
||||
)
|
||||
|
||||
# We want to purge everything, including local events, and to run the purge in
|
||||
# the background so that it's not blocking any other operation apart from
|
||||
# other purges in the same room.
|
||||
run_as_background_process(
|
||||
"_purge_history",
|
||||
self._purge_history,
|
||||
purge_id, room_id, token, True,
|
||||
)
|
||||
|
||||
def start_purge_history(self, room_id, token,
|
||||
delete_local_events=False):
|
||||
"""Start off a history purge on a room.
|
||||
|
||||
93
synapse/handlers/password_policy.py
Normal file
93
synapse/handlers/password_policy.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from synapse.api.errors import Codes, PasswordRefusedError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordPolicyHandler(object):
|
||||
def __init__(self, hs):
|
||||
self.policy = hs.config.password_policy
|
||||
self.enabled = hs.config.password_policy_enabled
|
||||
|
||||
# Regexps for the spec'd policy parameters.
|
||||
self.regexp_digit = re.compile("[0-9]")
|
||||
self.regexp_symbol = re.compile("[^a-zA-Z0-9]")
|
||||
self.regexp_uppercase = re.compile("[A-Z]")
|
||||
self.regexp_lowercase = re.compile("[a-z]")
|
||||
|
||||
def validate_password(self, password):
|
||||
"""Checks whether a given password complies with the server's policy.
|
||||
|
||||
Args:
|
||||
password (str): The password to check against the server's policy.
|
||||
|
||||
Raises:
|
||||
PasswordRefusedError: The password doesn't comply with the server's policy.
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
minimum_accepted_length = self.policy.get("minimum_length", 0)
|
||||
if len(password) < minimum_accepted_length:
|
||||
raise PasswordRefusedError(
|
||||
msg=(
|
||||
"The password must be at least %d characters long"
|
||||
% minimum_accepted_length
|
||||
),
|
||||
errcode=Codes.PASSWORD_TOO_SHORT,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_digit", False) and
|
||||
self.regexp_digit.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one digit",
|
||||
errcode=Codes.PASSWORD_NO_DIGIT,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_symbol", False) and
|
||||
self.regexp_symbol.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one symbol",
|
||||
errcode=Codes.PASSWORD_NO_SYMBOL,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_uppercase", False) and
|
||||
self.regexp_uppercase.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one uppercase letter",
|
||||
errcode=Codes.PASSWORD_NO_UPPERCASE,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_lowercase", False) and
|
||||
self.regexp_lowercase.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one lowercase letter",
|
||||
errcode=Codes.PASSWORD_NO_LOWERCASE,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +16,11 @@
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
from six.moves import range
|
||||
|
||||
from signedjson.sign import sign_json
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
@@ -26,6 +31,7 @@ from synapse.api.errors import (
|
||||
)
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.types import UserID, get_domain_from_id
|
||||
from synapse.util.logcontext import run_in_background
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
@@ -43,6 +49,8 @@ class BaseProfileHandler(BaseHandler):
|
||||
subclass MasterProfileHandler
|
||||
"""
|
||||
|
||||
PROFILE_REPLICATE_INTERVAL = 2 * 60 * 1000
|
||||
|
||||
def __init__(self, hs):
|
||||
super(BaseProfileHandler, self).__init__(hs)
|
||||
|
||||
@@ -53,6 +61,84 @@ class BaseProfileHandler(BaseHandler):
|
||||
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
if hs.config.worker_app is None:
|
||||
self.clock.looping_call(
|
||||
self._start_update_remote_profile_cache, self.PROFILE_UPDATE_MS,
|
||||
)
|
||||
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
reactor.callWhenRunning(self._assign_profile_replication_batches)
|
||||
reactor.callWhenRunning(self._replicate_profiles)
|
||||
# Add a looping call to replicate_profiles: this handles retries
|
||||
# if the replication is unsuccessful when the user updated their
|
||||
# profile.
|
||||
self.clock.looping_call(
|
||||
self._replicate_profiles, self.PROFILE_REPLICATE_INTERVAL
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _assign_profile_replication_batches(self):
|
||||
"""If no profile replication has been done yet, allocate replication batch
|
||||
numbers to each profile to start the replication process.
|
||||
"""
|
||||
logger.info("Assigning profile batch numbers...")
|
||||
total = 0
|
||||
while True:
|
||||
assigned = yield self.store.assign_profile_batch()
|
||||
total += assigned
|
||||
if assigned == 0:
|
||||
break
|
||||
logger.info("Assigned %d profile batch numbers", total)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _replicate_profiles(self):
|
||||
"""If any profile data has been updated and not pushed to the replication targets,
|
||||
replicate it.
|
||||
"""
|
||||
host_batches = yield self.store.get_replication_hosts()
|
||||
latest_batch = yield self.store.get_latest_profile_replication_batch_number()
|
||||
if latest_batch is None:
|
||||
latest_batch = -1
|
||||
for repl_host in self.hs.config.replicate_user_profiles_to:
|
||||
if repl_host not in host_batches:
|
||||
host_batches[repl_host] = -1
|
||||
try:
|
||||
for i in range(host_batches[repl_host] + 1, latest_batch + 1):
|
||||
yield self._replicate_host_profile_batch(repl_host, i)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Exception while replicating to %s: aborting for now", repl_host,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _replicate_host_profile_batch(self, host, batchnum):
|
||||
logger.info("Replicating profile batch %d to %s", batchnum, host)
|
||||
batch_rows = yield self.store.get_profile_batch(batchnum)
|
||||
batch = {
|
||||
UserID(r["user_id"], self.hs.hostname).to_string(): ({
|
||||
"display_name": r["displayname"],
|
||||
"avatar_url": r["avatar_url"],
|
||||
} if r["active"] else None) for r in batch_rows
|
||||
}
|
||||
|
||||
url = "https://%s/_matrix/identity/api/v1/replicate_profiles" % (host,)
|
||||
body = {
|
||||
"batchnum": batchnum,
|
||||
"batch": batch,
|
||||
"origin_server": self.hs.hostname,
|
||||
}
|
||||
signed_body = sign_json(body, self.hs.hostname, self.hs.config.signing_key[0])
|
||||
try:
|
||||
yield self.http_client.post_json_get_json(url, signed_body)
|
||||
yield self.store.update_replication_batch_for_host(host, batchnum)
|
||||
logger.info("Sucessfully replicated profile batch %d to %s", batchnum, host)
|
||||
except Exception:
|
||||
# This will get retried when the looping call next comes around
|
||||
logger.exception("Failed to replicate profile batch %d to %s", batchnum, host)
|
||||
raise
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_profile(self, user_id):
|
||||
target_user = UserID.from_string(user_id)
|
||||
@@ -162,9 +248,14 @@ class BaseProfileHandler(BaseHandler):
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
if not by_admin and target_user != requester.user:
|
||||
if not by_admin and requester and target_user != requester.user:
|
||||
raise AuthError(400, "Cannot set another user's displayname")
|
||||
|
||||
if not by_admin and self.hs.config.disable_set_displayname:
|
||||
profile = yield self.store.get_profileinfo(target_user.localpart)
|
||||
if profile.display_name:
|
||||
raise SynapseError(400, "Changing displayname is disabled on this server")
|
||||
|
||||
if len(new_displayname) > MAX_DISPLAYNAME_LEN:
|
||||
raise SynapseError(
|
||||
400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN, ),
|
||||
@@ -173,8 +264,14 @@ class BaseProfileHandler(BaseHandler):
|
||||
if new_displayname == '':
|
||||
new_displayname = None
|
||||
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
|
||||
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
|
||||
else:
|
||||
new_batchnum = None
|
||||
|
||||
yield self.store.set_profile_displayname(
|
||||
target_user.localpart, new_displayname
|
||||
target_user.localpart, new_displayname, new_batchnum
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
@@ -183,8 +280,38 @@ class BaseProfileHandler(BaseHandler):
|
||||
target_user.to_string(), profile
|
||||
)
|
||||
|
||||
if requester:
|
||||
yield self._update_join_states(requester, target_user)
|
||||
|
||||
# start a profile replication push
|
||||
run_in_background(self._replicate_profiles)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_active(self, target_user, active, hide):
|
||||
"""
|
||||
Sets the 'active' flag on a user profile. If set to false, the user
|
||||
account is considered deactivated or hidden.
|
||||
|
||||
If 'hide' is true, then we interpret active=False as a request to try to
|
||||
hide the user rather than deactivating it. This means withholding the
|
||||
profile from replication (and mark it as inactive) rather than clearing
|
||||
the profile from the HS DB. Note that unlike set_displayname and
|
||||
set_avatar_url, this does *not* perform authorization checks! This is
|
||||
because the only place it's used currently is in account deactivation
|
||||
where we've already done these checks anyway.
|
||||
"""
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
|
||||
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
|
||||
else:
|
||||
new_batchnum = None
|
||||
yield self.store.set_profile_active(
|
||||
target_user.localpart, active, hide, new_batchnum
|
||||
)
|
||||
|
||||
# start a profile replication push
|
||||
run_in_background(self._replicate_profiles)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_avatar_url(self, target_user):
|
||||
if self.hs.is_mine(target_user):
|
||||
@@ -225,13 +352,24 @@ class BaseProfileHandler(BaseHandler):
|
||||
if not by_admin and target_user != requester.user:
|
||||
raise AuthError(400, "Cannot set another user's avatar_url")
|
||||
|
||||
if not by_admin and self.hs.config.disable_set_avatar_url:
|
||||
profile = yield self.store.get_profileinfo(target_user.localpart)
|
||||
if profile.avatar_url:
|
||||
raise SynapseError(400, "Changing avatar url is disabled on this server")
|
||||
|
||||
if len(self.hs.config.replicate_user_profiles_to) > 0:
|
||||
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
|
||||
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
|
||||
else:
|
||||
new_batchnum = None
|
||||
|
||||
if len(new_avatar_url) > MAX_AVATAR_URL_LEN:
|
||||
raise SynapseError(
|
||||
400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN, ),
|
||||
)
|
||||
|
||||
yield self.store.set_profile_avatar_url(
|
||||
target_user.localpart, new_avatar_url
|
||||
target_user.localpart, new_avatar_url, new_batchnum,
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
@@ -242,6 +380,9 @@ class BaseProfileHandler(BaseHandler):
|
||||
|
||||
yield self._update_join_states(requester, target_user)
|
||||
|
||||
# start a profile replication push
|
||||
run_in_background(self._replicate_profiles)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_profile_query(self, args):
|
||||
user = UserID.from_string(args["user_id"])
|
||||
@@ -321,6 +462,10 @@ class BaseProfileHandler(BaseHandler):
|
||||
if not self.hs.config.require_auth_for_profile_requests or not requester:
|
||||
return
|
||||
|
||||
# Always allow the user to query their own profile.
|
||||
if target_user.to_string() == requester.to_string():
|
||||
return
|
||||
|
||||
try:
|
||||
requester_rooms = yield self.store.get_rooms_for_user(
|
||||
requester.to_string()
|
||||
|
||||
@@ -61,6 +61,7 @@ class RegistrationHandler(BaseHandler):
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
self.captcha_client = CaptchaServerHttpClient(hs)
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.identity_handler = self.hs.get_handlers().identity_handler
|
||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||
|
||||
@@ -73,6 +74,8 @@ class RegistrationHandler(BaseHandler):
|
||||
)
|
||||
self._server_notices_mxid = hs.config.server_notices_mxid
|
||||
|
||||
self._show_in_user_directory = self.hs.config.show_users_in_user_directory
|
||||
|
||||
if hs.config.worker_app:
|
||||
self._register_client = ReplicationRegisterServlet.make_client(hs)
|
||||
self._register_device_client = (
|
||||
@@ -234,6 +237,11 @@ class RegistrationHandler(BaseHandler):
|
||||
address=address,
|
||||
)
|
||||
|
||||
if default_display_name:
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, default_display_name, by_admin=True,
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
profile = yield self.store.get_profileinfo(localpart)
|
||||
yield self.user_directory_handler.handle_local_profile_change(
|
||||
@@ -263,6 +271,11 @@ class RegistrationHandler(BaseHandler):
|
||||
create_profile_with_displayname=default_display_name,
|
||||
address=address,
|
||||
)
|
||||
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, default_display_name, by_admin=True,
|
||||
)
|
||||
|
||||
except SynapseError:
|
||||
# if user id is taken, just generate another
|
||||
user = None
|
||||
@@ -287,6 +300,14 @@ class RegistrationHandler(BaseHandler):
|
||||
user_id, threepid_dict, None, False,
|
||||
)
|
||||
|
||||
# Prevent the new user from showing up in the user directory if the server
|
||||
# mandates it.
|
||||
if not self._show_in_user_directory:
|
||||
yield self.store.add_account_data_for_user(
|
||||
user_id, "im.vector.hide_profile", {'hide_profile': True},
|
||||
)
|
||||
yield self.profile_handler.set_active(user, False, True)
|
||||
|
||||
defer.returnValue((user_id, token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -356,7 +377,9 @@ class RegistrationHandler(BaseHandler):
|
||||
yield self._auto_join_rooms(user_id)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def appservice_register(self, user_localpart, as_token):
|
||||
def appservice_register(self, user_localpart, as_token, password, display_name):
|
||||
# FIXME: this should be factored out and merged with normal register()
|
||||
|
||||
user = UserID(user_localpart, self.hs.hostname)
|
||||
user_id = user.to_string()
|
||||
service = self.store.get_app_service_by_token(as_token)
|
||||
@@ -374,12 +397,29 @@ class RegistrationHandler(BaseHandler):
|
||||
user_id, allowed_appservice=service
|
||||
)
|
||||
|
||||
password_hash = ""
|
||||
if password:
|
||||
password_hash = yield self.auth_handler().hash(password)
|
||||
|
||||
display_name = display_name or user.localpart
|
||||
|
||||
yield self.register_with_store(
|
||||
user_id=user_id,
|
||||
password_hash="",
|
||||
password_hash=password_hash,
|
||||
appservice_id=service_id,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
create_profile_with_displayname=display_name,
|
||||
)
|
||||
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, display_name, by_admin=True,
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
profile = yield self.store.get_profileinfo(user_localpart)
|
||||
yield self.user_directory_handler.handle_local_profile_change(
|
||||
user_id, profile
|
||||
)
|
||||
|
||||
defer.returnValue(user_id)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -405,6 +445,39 @@ class RegistrationHandler(BaseHandler):
|
||||
else:
|
||||
logger.info("Valid captcha entered from %s", ip)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def register_saml2(self, localpart):
|
||||
"""
|
||||
Registers email_id as SAML2 Based Auth.
|
||||
"""
|
||||
if types.contains_invalid_mxid_characters(localpart):
|
||||
raise SynapseError(
|
||||
400,
|
||||
"User ID can only contain characters a-z, 0-9, or '=_-./'",
|
||||
)
|
||||
yield self.auth.check_auth_blocking()
|
||||
user = UserID(localpart, self.hs.hostname)
|
||||
user_id = user.to_string()
|
||||
|
||||
yield self.check_user_id_not_appservice_exclusive(user_id)
|
||||
token = self.macaroon_gen.generate_access_token(user_id)
|
||||
try:
|
||||
yield self.register_with_store(
|
||||
user_id=user_id,
|
||||
token=token,
|
||||
password_hash=None,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
)
|
||||
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, user.localpart, by_admin=True,
|
||||
)
|
||||
except Exception as e:
|
||||
yield self.store.add_access_token_to_user(user_id, token)
|
||||
# Ignore Registration errors
|
||||
logger.exception(e)
|
||||
defer.returnValue((user_id, token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def register_email(self, threepidCreds):
|
||||
"""
|
||||
@@ -427,7 +500,9 @@ class RegistrationHandler(BaseHandler):
|
||||
logger.info("got threepid with medium '%s' and address '%s'",
|
||||
threepid['medium'], threepid['address'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, threepid['medium'], threepid['address']):
|
||||
if not (
|
||||
yield check_3pid_allowed(self.hs, threepid['medium'], threepid['address'])
|
||||
):
|
||||
raise RegistrationError(
|
||||
403, "Third party identifier is not allowed"
|
||||
)
|
||||
@@ -468,6 +543,39 @@ class RegistrationHandler(BaseHandler):
|
||||
errcode=Codes.EXCLUSIVE
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_register(self, localpart, display_name, auth_result, params):
|
||||
"""Invokes the current registration on another server, using
|
||||
shared secret registration, passing in any auth_results from
|
||||
other registration UI auth flows (e.g. validated 3pids)
|
||||
Useful for setting up shadow/backup accounts on a parallel deployment.
|
||||
"""
|
||||
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/register?access_token=%s" % (
|
||||
shadow_hs_url, as_token,
|
||||
),
|
||||
{
|
||||
# XXX: auth_result is an unspecified extension for shadow registration
|
||||
'auth_result': auth_result,
|
||||
# XXX: another unspecified extension for shadow registration to ensure
|
||||
# that the displayname is correctly set by the masters erver
|
||||
'display_name': display_name,
|
||||
'username': localpart,
|
||||
'password': params.get("password"),
|
||||
'bind_email': params.get("bind_email"),
|
||||
'bind_msisdn': params.get("bind_msisdn"),
|
||||
'device_id': params.get("device_id"),
|
||||
'initial_device_display_name': params.get("initial_device_display_name"),
|
||||
'inhibit_login': False,
|
||||
'access_token': as_token,
|
||||
}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _generate_user_id(self, reseed=False):
|
||||
if reseed or self._next_generated_user_id is None:
|
||||
@@ -556,18 +664,16 @@ class RegistrationHandler(BaseHandler):
|
||||
user_id=user_id,
|
||||
token=token,
|
||||
password_hash=password_hash,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
create_profile_with_displayname=displayname or user.localpart,
|
||||
)
|
||||
if displayname is not None:
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, None, displayname or user.localpart, by_admin=True,
|
||||
)
|
||||
else:
|
||||
yield self._auth_handler.delete_access_tokens_for_user(user_id)
|
||||
yield self.store.add_access_token_to_user(user_id=user_id, token=token)
|
||||
|
||||
if displayname is not None:
|
||||
logger.info("setting user display name: %s -> %s", user_id, displayname)
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, requester, displayname, by_admin=True,
|
||||
)
|
||||
|
||||
defer.returnValue((user_id, token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
||||
@@ -49,12 +49,14 @@ class RoomCreationHandler(BaseHandler):
|
||||
"history_visibility": "shared",
|
||||
"original_invitees_have_ops": False,
|
||||
"guest_can_join": True,
|
||||
"encryption_alg": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
|
||||
"join_rules": JoinRules.INVITE,
|
||||
"history_visibility": "shared",
|
||||
"original_invitees_have_ops": True,
|
||||
"guest_can_join": True,
|
||||
"encryption_alg": "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
RoomCreationPreset.PUBLIC_CHAT: {
|
||||
"join_rules": JoinRules.PUBLIC,
|
||||
@@ -75,6 +77,10 @@ class RoomCreationHandler(BaseHandler):
|
||||
# linearizer to stop two upgrades happening at once
|
||||
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
|
||||
|
||||
self._server_notices_mxid = hs.config.server_notices_mxid
|
||||
|
||||
self.third_party_event_rules = hs.get_third_party_event_rules()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def upgrade_room(self, requester, old_room_id, new_version):
|
||||
"""Replace a room with a new room with a different version
|
||||
@@ -248,7 +254,22 @@ class RoomCreationHandler(BaseHandler):
|
||||
"""
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
if not self.spam_checker.user_may_create_room(user_id):
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to create rooms
|
||||
is_requester_admin = True
|
||||
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
if not is_requester_admin and not self.spam_checker.user_may_create_room(
|
||||
user_id,
|
||||
invite_list=[],
|
||||
third_party_invite_list=[],
|
||||
cloning=True,
|
||||
):
|
||||
raise SynapseError(403, "You are not permitted to create rooms")
|
||||
|
||||
creation_content = {
|
||||
@@ -470,7 +491,32 @@ class RoomCreationHandler(BaseHandler):
|
||||
|
||||
yield self.auth.check_auth_blocking(user_id)
|
||||
|
||||
if not self.spam_checker.user_may_create_room(user_id):
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to create rooms
|
||||
is_requester_admin = True
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
# Check whether the third party rules allows/changes the room create
|
||||
# request.
|
||||
yield self.third_party_event_rules.on_create_room(
|
||||
requester,
|
||||
config,
|
||||
is_requester_admin=is_requester_admin,
|
||||
)
|
||||
|
||||
invite_list = config.get("invite", [])
|
||||
invite_3pid_list = config.get("invite_3pid", [])
|
||||
|
||||
if not is_requester_admin and not self.spam_checker.user_may_create_room(
|
||||
user_id,
|
||||
invite_list=invite_list,
|
||||
third_party_invite_list=invite_3pid_list,
|
||||
cloning=False,
|
||||
):
|
||||
raise SynapseError(403, "You are not permitted to create rooms")
|
||||
|
||||
if ratelimit:
|
||||
@@ -517,7 +563,6 @@ class RoomCreationHandler(BaseHandler):
|
||||
else:
|
||||
room_alias = None
|
||||
|
||||
invite_list = config.get("invite", [])
|
||||
for i in invite_list:
|
||||
try:
|
||||
UserID.from_string(i)
|
||||
@@ -528,8 +573,6 @@ class RoomCreationHandler(BaseHandler):
|
||||
requester,
|
||||
)
|
||||
|
||||
invite_3pid_list = config.get("invite_3pid", [])
|
||||
|
||||
visibility = config.get("visibility", None)
|
||||
is_public = visibility == "public"
|
||||
|
||||
@@ -615,6 +658,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
"invite",
|
||||
ratelimit=False,
|
||||
content=content,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
for invite_3pid in invite_3pid_list:
|
||||
@@ -629,6 +673,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
id_server,
|
||||
requester,
|
||||
txn_id=None,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
result = {"room_id": room_id}
|
||||
@@ -699,6 +744,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
"join",
|
||||
ratelimit=False,
|
||||
content=creator_join_profile,
|
||||
new_room=True,
|
||||
)
|
||||
|
||||
# We treat the power levels override specially as this needs to be one
|
||||
@@ -774,6 +820,15 @@ class RoomCreationHandler(BaseHandler):
|
||||
content=content,
|
||||
)
|
||||
|
||||
if "encryption_alg" in config:
|
||||
yield send(
|
||||
etype=EventTypes.Encryption,
|
||||
state_key="",
|
||||
content={
|
||||
'algorithm': config["encryption_alg"],
|
||||
}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _generate_room_id(self, creator_id, is_public):
|
||||
# autogen room IDs and try to create it. We may clash, so just
|
||||
|
||||
@@ -20,16 +20,12 @@ import logging
|
||||
|
||||
from six.moves import http_client
|
||||
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
from signedjson.sign import verify_signed_json
|
||||
from unpaddedbase64 import decode_base64
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import synapse.server
|
||||
import synapse.types
|
||||
from synapse.api.constants import EventTypes, Membership
|
||||
from synapse.api.errors import AuthError, Codes, SynapseError
|
||||
from synapse.api.errors import AuthError, Codes, ProxiedRequestError, SynapseError
|
||||
from synapse.types import RoomID, UserID
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.distributor import user_joined_room, user_left_room
|
||||
@@ -67,12 +63,15 @@ class RoomMemberHandler(object):
|
||||
self.registration_handler = hs.get_registration_handler()
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
|
||||
self.member_linearizer = Linearizer(name="member")
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self.third_party_event_rules = hs.get_third_party_event_rules()
|
||||
self._server_notices_mxid = self.config.server_notices_mxid
|
||||
self.rewrite_identity_server_urls = self.config.rewrite_identity_server_urls
|
||||
self._enable_lookup = hs.config.enable_3pid_lookup
|
||||
self.allow_per_room_profiles = self.config.allow_per_room_profiles
|
||||
|
||||
@@ -317,8 +316,31 @@ class RoomMemberHandler(object):
|
||||
third_party_signed=None,
|
||||
ratelimit=True,
|
||||
content=None,
|
||||
new_room=False,
|
||||
require_consent=True,
|
||||
):
|
||||
"""Update a users membership in a room
|
||||
|
||||
Args:
|
||||
requester (Requester)
|
||||
target (UserID)
|
||||
room_id (str)
|
||||
action (str): The "action" the requester is performing against the
|
||||
target. One of join/leave/kick/ban/invite/unban.
|
||||
txn_id (str|None): The transaction ID associated with the request,
|
||||
or None not provided.
|
||||
remote_room_hosts (list[str]|None): List of remote servers to try
|
||||
and join via if server isn't already in the room.
|
||||
third_party_signed (dict|None): The signed object for third party
|
||||
invites.
|
||||
ratelimit (bool): Whether to apply ratelimiting to this request.
|
||||
content (dict|None): Fields to include in the new events content.
|
||||
new_room (bool): Whether these membership changes are happening
|
||||
as part of a room creation (e.g. initial joins and invites)
|
||||
|
||||
Returns:
|
||||
Deferred[FrozenEvent]
|
||||
"""
|
||||
key = (room_id,)
|
||||
|
||||
with (yield self.member_linearizer.queue(key)):
|
||||
@@ -332,6 +354,7 @@ class RoomMemberHandler(object):
|
||||
third_party_signed=third_party_signed,
|
||||
ratelimit=ratelimit,
|
||||
content=content,
|
||||
new_room=new_room,
|
||||
require_consent=require_consent,
|
||||
)
|
||||
|
||||
@@ -349,6 +372,7 @@ class RoomMemberHandler(object):
|
||||
third_party_signed=None,
|
||||
ratelimit=True,
|
||||
content=None,
|
||||
new_room=False,
|
||||
require_consent=True,
|
||||
):
|
||||
content_specified = bool(content)
|
||||
@@ -416,8 +440,14 @@ class RoomMemberHandler(object):
|
||||
)
|
||||
block_invite = True
|
||||
|
||||
is_published = yield self.store.is_room_published(room_id)
|
||||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), target.to_string(), room_id,
|
||||
requester.user.to_string(), target.to_string(),
|
||||
third_party_invite=None,
|
||||
room_id=room_id,
|
||||
new_room=new_room,
|
||||
published_room=is_published,
|
||||
):
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
block_invite = True
|
||||
@@ -496,8 +526,29 @@ class RoomMemberHandler(object):
|
||||
# so don't really fit into the general auth process.
|
||||
raise AuthError(403, "Guest access not allowed")
|
||||
|
||||
if not is_host_in_room:
|
||||
if (self._server_notices_mxid is not None and
|
||||
requester.user.to_string() == self._server_notices_mxid):
|
||||
# allow the server notices mxid to join rooms
|
||||
is_requester_admin = True
|
||||
|
||||
else:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
requester.user,
|
||||
)
|
||||
|
||||
inviter = yield self._get_inviter(target.to_string(), room_id)
|
||||
if not is_requester_admin:
|
||||
# We assume that if the spam checker allowed the user to create
|
||||
# a room then they're allowed to join it.
|
||||
if not new_room and not self.spam_checker.user_may_join_room(
|
||||
target.to_string(), room_id,
|
||||
is_invited=inviter is not None,
|
||||
):
|
||||
raise SynapseError(
|
||||
403, "Not allowed to join this room",
|
||||
)
|
||||
|
||||
if not is_host_in_room:
|
||||
if inviter and not self.hs.is_mine(inviter):
|
||||
remote_room_hosts.append(inviter.domain)
|
||||
|
||||
@@ -707,7 +758,8 @@ class RoomMemberHandler(object):
|
||||
address,
|
||||
id_server,
|
||||
requester,
|
||||
txn_id
|
||||
txn_id,
|
||||
new_room=False,
|
||||
):
|
||||
if self.config.block_non_admin_invites:
|
||||
is_requester_admin = yield self.auth.is_server_admin(
|
||||
@@ -723,10 +775,36 @@ class RoomMemberHandler(object):
|
||||
# can't just rely on the standard ratelimiting of events.
|
||||
yield self.base_handler.ratelimit(requester)
|
||||
|
||||
can_invite = yield self.third_party_event_rules.check_threepid_can_be_invited(
|
||||
medium, address, room_id,
|
||||
)
|
||||
if not can_invite:
|
||||
raise SynapseError(
|
||||
403, "This third-party identifier can not be invited in this room",
|
||||
Codes.FORBIDDEN,
|
||||
)
|
||||
|
||||
invitee = yield self._lookup_3pid(
|
||||
id_server, medium, address
|
||||
)
|
||||
|
||||
is_published = yield self.store.is_room_published(room_id)
|
||||
|
||||
if not self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), invitee,
|
||||
third_party_invite={
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
},
|
||||
room_id=room_id,
|
||||
new_room=new_room,
|
||||
published_room=is_published,
|
||||
):
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
raise SynapseError(
|
||||
403, "Invites have been disabled on this server",
|
||||
)
|
||||
|
||||
if invitee:
|
||||
yield self.update_membership(
|
||||
requester,
|
||||
@@ -746,6 +824,20 @@ class RoomMemberHandler(object):
|
||||
txn_id=txn_id
|
||||
)
|
||||
|
||||
def _get_id_server_target(self, id_server):
|
||||
"""Looks up an id_server's actual http endpoint
|
||||
|
||||
Args:
|
||||
id_server (str): the server name to lookup.
|
||||
|
||||
Returns:
|
||||
the http endpoint to connect to.
|
||||
"""
|
||||
if id_server in self.rewrite_identity_server_urls:
|
||||
return self.rewrite_identity_server_urls[id_server]
|
||||
|
||||
return id_server
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _lookup_3pid(self, id_server, medium, address):
|
||||
"""Looks up a 3pid in the passed identity server.
|
||||
@@ -759,48 +851,13 @@ class RoomMemberHandler(object):
|
||||
Returns:
|
||||
str: the matrix ID of the 3pid, or None if it is not recognized.
|
||||
"""
|
||||
if not self._enable_lookup:
|
||||
raise SynapseError(
|
||||
403, "Looking up third-party identifiers is denied from this server",
|
||||
)
|
||||
try:
|
||||
data = yield self.simple_http_client.get_json(
|
||||
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server,),
|
||||
{
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
}
|
||||
)
|
||||
|
||||
if "mxid" in data:
|
||||
if "signatures" not in data:
|
||||
raise AuthError(401, "No signatures on 3pid binding")
|
||||
yield self._verify_any_signature(data, id_server)
|
||||
defer.returnValue(data["mxid"])
|
||||
|
||||
except IOError as e:
|
||||
data = yield self.identity_handler.lookup_3pid(id_server, medium, address)
|
||||
defer.returnValue(data.get("mxid"))
|
||||
except ProxiedRequestError as e:
|
||||
logger.warn("Error from identity server lookup: %s" % (e,))
|
||||
defer.returnValue(None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _verify_any_signature(self, data, server_hostname):
|
||||
if server_hostname not in data["signatures"]:
|
||||
raise AuthError(401, "No signature from server %s" % (server_hostname,))
|
||||
for key_name, signature in data["signatures"][server_hostname].items():
|
||||
key_data = yield self.simple_http_client.get_json(
|
||||
"%s%s/_matrix/identity/api/v1/pubkey/%s" %
|
||||
(id_server_scheme, server_hostname, key_name,),
|
||||
)
|
||||
if "public_key" not in key_data:
|
||||
raise AuthError(401, "No public key named %s from %s" %
|
||||
(key_name, server_hostname,))
|
||||
verify_signed_json(
|
||||
data,
|
||||
server_hostname,
|
||||
decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
|
||||
)
|
||||
return
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _make_and_store_3pid_invite(
|
||||
self,
|
||||
@@ -878,6 +935,7 @@ class RoomMemberHandler(object):
|
||||
"sender": user.to_string(),
|
||||
"state_key": token,
|
||||
},
|
||||
ratelimit=False,
|
||||
txn_id=txn_id,
|
||||
)
|
||||
|
||||
@@ -926,8 +984,9 @@ class RoomMemberHandler(object):
|
||||
user.
|
||||
"""
|
||||
|
||||
target = self._get_id_server_target(id_server)
|
||||
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
|
||||
id_server_scheme, id_server,
|
||||
id_server_scheme, target,
|
||||
)
|
||||
|
||||
invite_config = {
|
||||
@@ -967,7 +1026,7 @@ class RoomMemberHandler(object):
|
||||
fallback_public_key = {
|
||||
"public_key": data["public_key"],
|
||||
"key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
|
||||
id_server_scheme, id_server,
|
||||
id_server_scheme, target,
|
||||
),
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 New Vector Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -29,9 +30,12 @@ class SetPasswordHandler(BaseHandler):
|
||||
super(SetPasswordHandler, self).__init__(hs)
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
self._device_handler = hs.get_device_handler()
|
||||
self._password_policy_handler = hs.get_password_policy_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_password(self, user_id, newpassword, requester=None):
|
||||
self._password_policy_handler.validate_password(newpassword)
|
||||
|
||||
password_hash = yield self._auth_handler.hash(newpassword)
|
||||
|
||||
except_device_id = requester.device_id if requester else None
|
||||
|
||||
@@ -117,7 +117,7 @@ class Mailer(object):
|
||||
|
||||
link = (
|
||||
self.hs.config.public_baseurl +
|
||||
"_synapse/password_reset/email/submit_token"
|
||||
"_matrix/client/unstable/password_reset/email/submit_token"
|
||||
"?token=%s&client_secret=%s&sid=%s" %
|
||||
(token, client_secret, sid)
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ REQUIREMENTS = [
|
||||
"pymacaroons>=0.13.0",
|
||||
"msgpack>=0.5.0",
|
||||
"phonenumbers>=8.2.0",
|
||||
"six>=1.10",
|
||||
"six>=1.12",
|
||||
# prometheus_client 0.4.0 changed the format of counter metrics
|
||||
# (cf https://github.com/matrix-org/synapse/issues/4001)
|
||||
"prometheus_client>=0.0.18,<0.4.0",
|
||||
@@ -80,7 +80,7 @@ REQUIREMENTS = [
|
||||
]
|
||||
|
||||
CONDITIONAL_REQUIREMENTS = {
|
||||
"email": ["Jinja2>=2.9", "bleach>=1.4.2"],
|
||||
"email": ["Jinja2>=2.9", "bleach>=1.4.3"],
|
||||
"matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],
|
||||
|
||||
# we use execute_batch, which arrived in psycopg 2.7.
|
||||
|
||||
1
synapse/res/templates/account_renewed.html
Normal file
1
synapse/res/templates/account_renewed.html
Normal file
@@ -0,0 +1 @@
|
||||
<html><body>Your account has been successfully renewed.</body><html>
|
||||
1
synapse/res/templates/invalid_token.html
Normal file
1
synapse/res/templates/invalid_token.html
Normal file
@@ -0,0 +1 @@
|
||||
<html><body>Invalid renewal token.</body><html>
|
||||
@@ -1,6 +1,6 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>Your password was successfully reset. You may now close this window.</p>
|
||||
<p>Your email has now been validated, please return to your client to reset your password. You may now close this window.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,6 +41,7 @@ from synapse.rest.client.v2_alpha import (
|
||||
keys,
|
||||
notifications,
|
||||
openid,
|
||||
password_policy,
|
||||
read_marker,
|
||||
receipts,
|
||||
register,
|
||||
@@ -116,6 +117,7 @@ class ClientRestResource(JsonResource):
|
||||
room_upgrade_rest_servlet.register_servlets(hs, client_resource)
|
||||
capabilities.register_servlets(hs, client_resource)
|
||||
account_validity.register_servlets(hs, client_resource)
|
||||
password_policy.register_servlets(hs, client_resource)
|
||||
relations.register_servlets(hs, client_resource)
|
||||
|
||||
# moving to /_synapse/admin
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
# limitations under the License.
|
||||
|
||||
""" This module contains REST servlets to do with profile: /profile/<paths> """
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.rest.client.v2_alpha._base import client_patterns
|
||||
from synapse.types import UserID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProfileDisplaynameRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True)
|
||||
@@ -28,6 +32,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
|
||||
super(ProfileDisplaynameRestServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -66,11 +71,30 @@ class ProfileDisplaynameRestServlet(RestServlet):
|
||||
yield self.profile_handler.set_displayname(
|
||||
user, requester, new_name, is_admin)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_displayname(shadow_user.to_string(), content)
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
def on_OPTIONS(self, request, user_id):
|
||||
return (200, {})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_displayname(self, user_id, body):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.put_json(
|
||||
"%s/_matrix/client/r0/profile/%s/displayname?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, user_id, as_token, user_id
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ProfileAvatarURLRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/avatar_url", v1=True)
|
||||
@@ -79,6 +103,7 @@ class ProfileAvatarURLRestServlet(RestServlet):
|
||||
super(ProfileAvatarURLRestServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -116,11 +141,30 @@ class ProfileAvatarURLRestServlet(RestServlet):
|
||||
yield self.profile_handler.set_avatar_url(
|
||||
user, requester, new_name, is_admin)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_avatar_url(shadow_user.to_string(), content)
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
def on_OPTIONS(self, request, user_id):
|
||||
return (200, {})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_avatar_url(self, user_id, body):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.put_json(
|
||||
"%s/_matrix/client/r0/profile/%s/avatar_url?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, user_id, as_token, user_id
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ProfileRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)", v1=True)
|
||||
|
||||
@@ -320,7 +320,7 @@ class PublicRoomListRestServlet(TransactionRestServlet):
|
||||
# Option to allow servers to require auth when accessing
|
||||
# /publicRooms via CS API. This is especially helpful in private
|
||||
# federations.
|
||||
if self.hs.config.restrict_public_rooms_to_local_users:
|
||||
if not self.hs.config.allow_public_rooms_without_auth:
|
||||
raise
|
||||
|
||||
# We allow people to not be authed if they're just looking at our
|
||||
@@ -704,7 +704,8 @@ class RoomMembershipRestServlet(TransactionRestServlet):
|
||||
content["address"],
|
||||
content["id_server"],
|
||||
requester,
|
||||
txn_id
|
||||
txn_id,
|
||||
new_room=False,
|
||||
)
|
||||
defer.returnValue((200, {}))
|
||||
return
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2018, 2019 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +32,7 @@ from synapse.http.servlet import (
|
||||
parse_json_object_from_request,
|
||||
parse_string,
|
||||
)
|
||||
from synapse.types import UserID
|
||||
from synapse.util.msisdn import phone_number_to_msisdn
|
||||
from synapse.util.stringutils import random_string
|
||||
from synapse.util.threepids import check_3pid_allowed
|
||||
@@ -85,7 +86,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
|
||||
if not check_3pid_allowed(self.hs, "email", email):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized on this server",
|
||||
"Your email is not authorized on this server",
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
@@ -208,7 +209,7 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
|
||||
|
||||
msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "msisdn", msisdn):
|
||||
if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Account phone numbers are not authorized on this server",
|
||||
@@ -228,9 +229,11 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
|
||||
|
||||
class PasswordResetSubmitTokenServlet(RestServlet):
|
||||
"""Handles 3PID validation token submission"""
|
||||
PATTERNS = [
|
||||
re.compile("^/_synapse/password_reset/(?P<medium>[^/]*)/submit_token/*$"),
|
||||
]
|
||||
PATTERNS = client_patterns(
|
||||
"/password_reset/(?P<medium>[^/]*)/submit_token/*$",
|
||||
releases=(),
|
||||
unstable=True,
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
@@ -348,6 +351,7 @@ class PasswordRestServlet(RestServlet):
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.datastore = self.hs.get_datastore()
|
||||
self._set_password_handler = hs.get_set_password_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@interactive_auth_handler
|
||||
@defer.inlineCallbacks
|
||||
@@ -366,6 +370,10 @@ class PasswordRestServlet(RestServlet):
|
||||
|
||||
if self.auth.has_access_token(request):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
# blindly trust ASes without UI-authing them
|
||||
if requester.app_service:
|
||||
params = body
|
||||
else:
|
||||
params = yield self.auth_handler.validate_user_via_ui_auth(
|
||||
requester, body, self.hs.get_ip_from_request(request),
|
||||
)
|
||||
@@ -405,11 +413,30 @@ class PasswordRestServlet(RestServlet):
|
||||
user_id, new_password, requester
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_password(params, shadow_user.to_string())
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
def on_OPTIONS(self, _):
|
||||
return 200, {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_password(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/password?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, as_token, user_id,
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class DeactivateAccountRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/deactivate$")
|
||||
@@ -476,10 +503,10 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
|
||||
['id_server', 'client_secret', 'email', 'send_attempt'],
|
||||
)
|
||||
|
||||
if not check_3pid_allowed(self.hs, "email", body['email']):
|
||||
if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized on this server",
|
||||
"Your email is not authorized on this server",
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
@@ -513,7 +540,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
|
||||
|
||||
msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "msisdn", msisdn):
|
||||
if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Account phone numbers are not authorized on this server",
|
||||
@@ -540,7 +567,8 @@ class ThreepidRestServlet(RestServlet):
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
self.auth = hs.get_auth()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.datastore = self.hs.get_datastore()
|
||||
self.datastore = hs.get_datastore()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
@@ -554,16 +582,21 @@ class ThreepidRestServlet(RestServlet):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
if self.hs.config.disable_3pid_changes:
|
||||
raise SynapseError(400, "3PID changes disabled on this server")
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
# skip validation if this is a shadow 3PID from an AS
|
||||
if not requester.app_service:
|
||||
threePidCreds = body.get('threePidCreds')
|
||||
threePidCreds = body.get('three_pid_creds', threePidCreds)
|
||||
if threePidCreds is None:
|
||||
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
|
||||
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds)
|
||||
|
||||
if not threepid:
|
||||
@@ -575,6 +608,12 @@ class ThreepidRestServlet(RestServlet):
|
||||
if reqd not in threepid:
|
||||
logger.warn("Couldn't add 3pid: invalid response from ID server")
|
||||
raise SynapseError(500, "Invalid response from ID Server")
|
||||
else:
|
||||
# XXX: ASes pass in a validated threepid directly to bypass the IS.
|
||||
# This makes the API entirely change shape when we have an AS token;
|
||||
# it really should be an entirely separate API - perhaps
|
||||
# /account/3pid/replicate or something.
|
||||
threepid = body.get('threepid')
|
||||
|
||||
yield self.auth_handler.add_threepid(
|
||||
user_id,
|
||||
@@ -583,7 +622,7 @@ class ThreepidRestServlet(RestServlet):
|
||||
threepid['validated_at'],
|
||||
)
|
||||
|
||||
if 'bind' in body and body['bind']:
|
||||
if not requester.app_service and ('bind' in body and body['bind']):
|
||||
logger.debug(
|
||||
"Binding threepid %s to %s",
|
||||
threepid, user_id
|
||||
@@ -592,19 +631,43 @@ class ThreepidRestServlet(RestServlet):
|
||||
threePidCreds, user_id
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_3pid({'threepid': threepid}, shadow_user.to_string())
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_3pid(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, as_token, user_id,
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ThreepidDeleteRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/3pid/delete$")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ThreepidDeleteRestServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
if self.hs.config.disable_3pid_changes:
|
||||
raise SynapseError(400, "3PID changes disabled on this server")
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(body, ['medium', 'address'])
|
||||
|
||||
@@ -622,6 +685,12 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
logger.exception("Failed to remove threepid")
|
||||
raise SynapseError(500, "Failed to remove threepid")
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
self.shadow_3pid_delete(body, shadow_user.to_string())
|
||||
|
||||
if ret:
|
||||
id_server_unbind_result = "success"
|
||||
else:
|
||||
@@ -631,6 +700,78 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
"id_server_unbind_result": id_server_unbind_result,
|
||||
}))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_3pid_delete(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid/delete?access_token=%s&user_id=%s" % (
|
||||
shadow_hs_url, as_token, user_id
|
||||
),
|
||||
body
|
||||
)
|
||||
|
||||
|
||||
class ThreepidLookupRestServlet(RestServlet):
|
||||
PATTERNS = [re.compile("^/_matrix/client/unstable/account/3pid/lookup$")]
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ThreepidLookupRestServlet, self).__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
"""Proxy a /_matrix/identity/api/v1/lookup request to an identity
|
||||
server
|
||||
"""
|
||||
yield self.auth.get_user_by_req(request)
|
||||
|
||||
# Verify query parameters
|
||||
query_params = request.args
|
||||
assert_params_in_dict(query_params, [b"medium", b"address", b"id_server"])
|
||||
|
||||
# Retrieve needed information from query parameters
|
||||
medium = parse_string(request, "medium")
|
||||
address = parse_string(request, "address")
|
||||
id_server = parse_string(request, "id_server")
|
||||
|
||||
# Proxy the request to the identity server. lookup_3pid handles checking
|
||||
# if the lookup is allowed so we don't need to do it here.
|
||||
ret = yield self.identity_handler.lookup_3pid(id_server, medium, address)
|
||||
|
||||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class ThreepidBulkLookupRestServlet(RestServlet):
|
||||
PATTERNS = [re.compile("^/_matrix/client/unstable/account/3pid/bulk_lookup$")]
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ThreepidBulkLookupRestServlet, self).__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
"""Proxy a /_matrix/identity/api/v1/bulk_lookup request to an identity
|
||||
server
|
||||
"""
|
||||
yield self.auth.get_user_by_req(request)
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
assert_params_in_dict(body, ["threepids", "id_server"])
|
||||
|
||||
# Proxy the request to the identity server. lookup_3pid handles checking
|
||||
# if the lookup is allowed so we don't need to do it here.
|
||||
ret = yield self.identity_handler.bulk_lookup_3pid(
|
||||
body["id_server"], body["threepids"],
|
||||
)
|
||||
|
||||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class WhoamiRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/whoami$")
|
||||
@@ -656,4 +797,6 @@ def register_servlets(hs, http_server):
|
||||
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
|
||||
ThreepidRestServlet(hs).register(http_server)
|
||||
ThreepidDeleteRestServlet(hs).register(http_server)
|
||||
ThreepidLookupRestServlet(hs).register(http_server)
|
||||
ThreepidBulkLookupRestServlet(hs).register(http_server)
|
||||
WhoamiRestServlet(hs).register(http_server)
|
||||
|
||||
@@ -19,6 +19,7 @@ from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import AuthError, NotFoundError, SynapseError
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.types import UserID
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
@@ -39,6 +40,7 @@ class AccountDataServlet(RestServlet):
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
self.notifier = hs.get_notifier()
|
||||
self._profile_handler = hs.get_profile_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, user_id, account_data_type):
|
||||
@@ -48,6 +50,11 @@ class AccountDataServlet(RestServlet):
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if account_data_type == "im.vector.hide_profile":
|
||||
user = UserID.from_string(user_id)
|
||||
hide_profile = body.get('hide_profile')
|
||||
yield self._profile_handler.set_active(user, not hide_profile, True)
|
||||
|
||||
max_id = yield self.store.add_account_data_for_user(
|
||||
user_id, account_data_type, body
|
||||
)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
import logging
|
||||
|
||||
from six import ensure_binary
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import AuthError, SynapseError
|
||||
@@ -40,6 +42,8 @@ class AccountValidityRenewServlet(RestServlet):
|
||||
self.hs = hs
|
||||
self.account_activity_handler = hs.get_account_validity_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self.success_html = hs.config.account_validity.account_renewed_html_content
|
||||
self.failure_html = hs.config.account_validity.invalid_token_html_content
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
@@ -47,14 +51,21 @@ class AccountValidityRenewServlet(RestServlet):
|
||||
raise SynapseError(400, "Missing renewal token")
|
||||
renewal_token = request.args[b"token"][0]
|
||||
|
||||
yield self.account_activity_handler.renew_account(renewal_token.decode('utf8'))
|
||||
token_valid = yield self.account_activity_handler.renew_account(
|
||||
renewal_token.decode("utf8")
|
||||
)
|
||||
|
||||
request.setResponseCode(200)
|
||||
if token_valid:
|
||||
status_code = 200
|
||||
response = self.success_html
|
||||
else:
|
||||
status_code = 404
|
||||
response = self.failure_html
|
||||
|
||||
request.setResponseCode(status_code)
|
||||
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
|
||||
request.setHeader(b"Content-Length", b"%d" % (
|
||||
len(AccountValidityRenewServlet.SUCCESS_HTML),
|
||||
))
|
||||
request.write(AccountValidityRenewServlet.SUCCESS_HTML)
|
||||
request.setHeader(b"Content-Length", b"%d" % (len(response),))
|
||||
request.write(ensure_binary(response))
|
||||
finish_request(request)
|
||||
defer.returnValue(None)
|
||||
|
||||
@@ -79,7 +90,7 @@ class AccountValiditySendMailServlet(RestServlet):
|
||||
if not self.account_validity.renew_by_email_enabled:
|
||||
raise AuthError(403, "Account renewal via email is disabled on this server.")
|
||||
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
requester = yield self.auth.get_user_by_req(request, allow_expired=True)
|
||||
user_id = requester.user.to_string()
|
||||
yield self.account_activity_handler.send_renewal_email_to_user(user_id)
|
||||
|
||||
|
||||
58
synapse/rest/client/v2_alpha/password_policy.py
Normal file
58
synapse/rest/client/v2_alpha/password_policy.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from synapse.http.servlet import RestServlet
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordPolicyServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/password_policy$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(PasswordPolicyServlet, self).__init__()
|
||||
|
||||
self.policy = hs.config.password_policy
|
||||
self.enabled = hs.config.password_policy_enabled
|
||||
|
||||
def on_GET(self, request):
|
||||
if not self.enabled or not self.policy:
|
||||
return (200, {})
|
||||
|
||||
policy = {}
|
||||
|
||||
for param in [
|
||||
"minimum_length",
|
||||
"require_digit",
|
||||
"require_symbol",
|
||||
"require_lowercase",
|
||||
"require_uppercase",
|
||||
]:
|
||||
if param in self.policy:
|
||||
policy["m.%s" % param] = self.policy[param]
|
||||
|
||||
return (200, policy)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
PasswordPolicyServlet(hs).register(http_server)
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 - 2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2015-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +17,9 @@
|
||||
|
||||
import hmac
|
||||
import logging
|
||||
import re
|
||||
from hashlib import sha1
|
||||
from string import capwords
|
||||
|
||||
from six import string_types
|
||||
|
||||
@@ -79,10 +82,10 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
|
||||
'id_server', 'client_secret', 'email', 'send_attempt'
|
||||
])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "email", body['email']):
|
||||
if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized to register on this server",
|
||||
"Your email is not authorized to register on this server",
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
@@ -121,7 +124,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
|
||||
|
||||
msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "msisdn", msisdn):
|
||||
if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Phone numbers are not authorized to register on this server",
|
||||
@@ -200,6 +203,7 @@ class RegisterRestServlet(RestServlet):
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.macaroon_gen = hs.get_macaroon_generator()
|
||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||
self.password_policy_handler = hs.get_password_policy_handler()
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
@interactive_auth_handler
|
||||
@@ -243,6 +247,7 @@ class RegisterRestServlet(RestServlet):
|
||||
if (not isinstance(body['password'], string_types) or
|
||||
len(body['password']) > 512):
|
||||
raise SynapseError(400, "Invalid password")
|
||||
self.password_policy_handler.validate_password(body['password'])
|
||||
desired_password = body["password"]
|
||||
|
||||
desired_username = None
|
||||
@@ -252,6 +257,8 @@ class RegisterRestServlet(RestServlet):
|
||||
raise SynapseError(400, "Invalid username")
|
||||
desired_username = body['username']
|
||||
|
||||
desired_display_name = body.get('display_name')
|
||||
|
||||
appservice = None
|
||||
if self.auth.has_access_token(request):
|
||||
appservice = yield self.auth.get_appservice_by_req(request)
|
||||
@@ -275,7 +282,8 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
if isinstance(desired_username, string_types):
|
||||
result = yield self._do_appservice_registration(
|
||||
desired_username, access_token, body
|
||||
desired_username, desired_password, desired_display_name,
|
||||
access_token, body
|
||||
)
|
||||
defer.returnValue((200, result)) # we throw for non 200 responses
|
||||
return
|
||||
@@ -413,7 +421,7 @@ class RegisterRestServlet(RestServlet):
|
||||
medium = auth_result[login_type]['medium']
|
||||
address = auth_result[login_type]['address']
|
||||
|
||||
if not check_3pid_allowed(self.hs, medium, address):
|
||||
if not (yield check_3pid_allowed(self.hs, medium, address)):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Third party identifiers (email/phone numbers)" +
|
||||
@@ -421,6 +429,95 @@ class RegisterRestServlet(RestServlet):
|
||||
Codes.THREEPID_DENIED,
|
||||
)
|
||||
|
||||
existingUid = yield self.store.get_user_id_by_threepid(
|
||||
medium, address,
|
||||
)
|
||||
|
||||
if existingUid is not None:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"%s is already in use" % medium,
|
||||
Codes.THREEPID_IN_USE,
|
||||
)
|
||||
|
||||
if self.hs.config.register_mxid_from_3pid:
|
||||
# override the desired_username based on the 3PID if any.
|
||||
# reset it first to avoid folks picking their own username.
|
||||
desired_username = None
|
||||
|
||||
# we should have an auth_result at this point if we're going to progress
|
||||
# to register the user (i.e. we haven't picked up a registered_user_id
|
||||
# from our session store), in which case get ready and gen the
|
||||
# desired_username
|
||||
if auth_result:
|
||||
if (
|
||||
self.hs.config.register_mxid_from_3pid == 'email' and
|
||||
LoginType.EMAIL_IDENTITY in auth_result
|
||||
):
|
||||
address = auth_result[LoginType.EMAIL_IDENTITY]['address']
|
||||
desired_username = synapse.types.strip_invalid_mxid_characters(
|
||||
address.replace('@', '-').lower()
|
||||
)
|
||||
|
||||
# find a unique mxid for the account, suffixing numbers
|
||||
# if needed
|
||||
while True:
|
||||
try:
|
||||
yield self.registration_handler.check_username(
|
||||
desired_username,
|
||||
guest_access_token=guest_access_token,
|
||||
assigned_user_id=registered_user_id,
|
||||
)
|
||||
# if we got this far we passed the check.
|
||||
break
|
||||
except SynapseError as e:
|
||||
if e.errcode == Codes.USER_IN_USE:
|
||||
m = re.match(r'^(.*?)(\d+)$', desired_username)
|
||||
if m:
|
||||
desired_username = m.group(1) + str(
|
||||
int(m.group(2)) + 1
|
||||
)
|
||||
else:
|
||||
desired_username += "1"
|
||||
else:
|
||||
# something else went wrong.
|
||||
break
|
||||
|
||||
if self.hs.config.register_just_use_email_for_display_name:
|
||||
desired_display_name = address
|
||||
else:
|
||||
# XXX: a nasty heuristic to turn an email address into
|
||||
# a displayname, as part of register_mxid_from_3pid
|
||||
parts = address.replace('.', ' ').split('@')
|
||||
org_parts = parts[1].split(' ')
|
||||
|
||||
if org_parts[-2] == "matrix" and org_parts[-1] == "org":
|
||||
org = "Tchap Admin"
|
||||
elif org_parts[-2] == "gouv" and org_parts[-1] == "fr":
|
||||
org = org_parts[-3] if len(org_parts) > 2 else org_parts[-2]
|
||||
else:
|
||||
org = org_parts[-2]
|
||||
|
||||
desired_display_name = (
|
||||
capwords(parts[0]) + " [" + capwords(org) + "]"
|
||||
)
|
||||
elif (
|
||||
self.hs.config.register_mxid_from_3pid == 'msisdn' and
|
||||
LoginType.MSISDN in auth_result
|
||||
):
|
||||
desired_username = auth_result[LoginType.MSISDN]['address']
|
||||
else:
|
||||
raise SynapseError(
|
||||
400, "Cannot derive mxid from 3pid; no recognised 3pid"
|
||||
)
|
||||
|
||||
if desired_username is not None:
|
||||
yield self.registration_handler.check_username(
|
||||
desired_username,
|
||||
guest_access_token=guest_access_token,
|
||||
assigned_user_id=registered_user_id,
|
||||
)
|
||||
|
||||
if registered_user_id is not None:
|
||||
logger.info(
|
||||
"Already registered user ID %r for this session",
|
||||
@@ -432,9 +529,16 @@ class RegisterRestServlet(RestServlet):
|
||||
# NB: This may be from the auth handler and NOT from the POST
|
||||
assert_params_in_dict(params, ["password"])
|
||||
|
||||
if not self.hs.config.register_mxid_from_3pid:
|
||||
desired_username = params.get("username", None)
|
||||
else:
|
||||
# we keep the original desired_username derived from the 3pid above
|
||||
pass
|
||||
|
||||
guest_access_token = params.get("guest_access_token", None)
|
||||
new_password = params.get("password", None)
|
||||
|
||||
# XXX: don't we need to validate these for length etc like we did on
|
||||
# the ones from the JSON body earlier on in the method?
|
||||
|
||||
if desired_username is not None:
|
||||
desired_username = desired_username.lower()
|
||||
@@ -467,9 +571,10 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
(registered_user_id, _) = yield self.registration_handler.register(
|
||||
localpart=desired_username,
|
||||
password=new_password,
|
||||
password=params.get("password", None),
|
||||
guest_access_token=guest_access_token,
|
||||
generate_token=False,
|
||||
default_display_name=desired_display_name,
|
||||
threepid=threepid,
|
||||
address=client_addr,
|
||||
)
|
||||
@@ -481,6 +586,14 @@ class RegisterRestServlet(RestServlet):
|
||||
):
|
||||
yield self.store.upsert_monthly_active_user(registered_user_id)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
yield self.registration_handler.shadow_register(
|
||||
localpart=desired_username,
|
||||
display_name=desired_display_name,
|
||||
auth_result=auth_result,
|
||||
params=params,
|
||||
)
|
||||
|
||||
# remember that we've now registered that user account, and with
|
||||
# what user ID (since the user may not have specified)
|
||||
self.auth_handler.set_session_data(
|
||||
@@ -508,11 +621,33 @@ class RegisterRestServlet(RestServlet):
|
||||
return 200, {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_appservice_registration(self, username, as_token, body):
|
||||
def _do_appservice_registration(
|
||||
self, username, password, display_name, as_token, body
|
||||
):
|
||||
|
||||
# FIXME: appservice_register() is horribly duplicated with register()
|
||||
# and they should probably just be combined together with a config flag.
|
||||
user_id = yield self.registration_handler.appservice_register(
|
||||
username, as_token
|
||||
username, as_token, password, display_name
|
||||
)
|
||||
defer.returnValue((yield self._create_registration_details(user_id, body)))
|
||||
result = yield self._create_registration_details(user_id, body)
|
||||
|
||||
auth_result = body.get('auth_result')
|
||||
if auth_result and LoginType.EMAIL_IDENTITY in auth_result:
|
||||
threepid = auth_result[LoginType.EMAIL_IDENTITY]
|
||||
yield self._register_email_threepid(
|
||||
user_id, threepid, result["access_token"],
|
||||
body.get("bind_email")
|
||||
)
|
||||
|
||||
if auth_result and LoginType.MSISDN in auth_result:
|
||||
threepid = auth_result[LoginType.MSISDN]
|
||||
yield self._register_msisdn_threepid(
|
||||
user_id, threepid, result["access_token"],
|
||||
body.get("bind_msisdn")
|
||||
)
|
||||
|
||||
defer.returnValue(result)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_shared_secret_registration(self, username, password, body):
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
import logging
|
||||
|
||||
from signedjson.sign import sign_json
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
@@ -37,6 +39,7 @@ class UserDirectorySearchRestServlet(RestServlet):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.user_directory_handler = hs.get_user_directory_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
@@ -67,6 +70,14 @@ class UserDirectorySearchRestServlet(RestServlet):
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if self.hs.config.user_directory_defer_to_id_server:
|
||||
signed_body = sign_json(body, self.hs.hostname, self.hs.config.signing_key[0])
|
||||
url = "%s/_matrix/identity/api/v1/user_directory/search" % (
|
||||
self.hs.config.user_directory_defer_to_id_server,
|
||||
)
|
||||
resp = yield self.http_client.post_json_get_json(url, signed_body)
|
||||
defer.returnValue((200, resp))
|
||||
|
||||
limit = body.get("limit", 10)
|
||||
limit = min(limit, 50)
|
||||
|
||||
|
||||
0
synapse/rulecheck/__init__.py
Normal file
0
synapse/rulecheck/__init__.py
Normal file
172
synapse/rulecheck/domain_rule_checker.py
Normal file
172
synapse/rulecheck/domain_rule_checker.py
Normal file
@@ -0,0 +1,172 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from synapse.config._base import ConfigError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainRuleChecker(object):
|
||||
"""
|
||||
A re-implementation of the SpamChecker that prevents users in one domain from
|
||||
inviting users in other domains to rooms, based on a configuration.
|
||||
|
||||
Takes a config in the format:
|
||||
|
||||
spam_checker:
|
||||
module: "rulecheck.DomainRuleChecker"
|
||||
config:
|
||||
domain_mapping:
|
||||
"inviter_domain": [ "invitee_domain_permitted", "other_domain_permitted" ]
|
||||
"other_inviter_domain": [ "invitee_domain_permitted" ]
|
||||
default: False
|
||||
|
||||
# Only let local users join rooms if they were explicitly invited.
|
||||
can_only_join_rooms_with_invite: false
|
||||
|
||||
# Only let local users create rooms if they are inviting only one
|
||||
# other user, and that user matches the rules above.
|
||||
can_only_create_one_to_one_rooms: false
|
||||
|
||||
# Only let local users invite during room creation, regardless of the
|
||||
# domain mapping rules above.
|
||||
can_only_invite_during_room_creation: false
|
||||
|
||||
# Prevent local users from inviting users from certain domains to
|
||||
# rooms published in the room directory.
|
||||
domains_prevented_from_being_invited_to_published_rooms: []
|
||||
|
||||
# Allow third party invites
|
||||
can_invite_by_third_party_id: true
|
||||
|
||||
Don't forget to consider if you can invite users from your own domain.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.domain_mapping = config["domain_mapping"] or {}
|
||||
self.default = config["default"]
|
||||
|
||||
self.can_only_join_rooms_with_invite = config.get(
|
||||
"can_only_join_rooms_with_invite", False,
|
||||
)
|
||||
self.can_only_create_one_to_one_rooms = config.get(
|
||||
"can_only_create_one_to_one_rooms", False,
|
||||
)
|
||||
self.can_only_invite_during_room_creation = config.get(
|
||||
"can_only_invite_during_room_creation", False,
|
||||
)
|
||||
self.can_invite_by_third_party_id = config.get(
|
||||
"can_invite_by_third_party_id", True,
|
||||
)
|
||||
self.domains_prevented_from_being_invited_to_published_rooms = config.get(
|
||||
"domains_prevented_from_being_invited_to_published_rooms", [],
|
||||
)
|
||||
|
||||
def check_event_for_spam(self, event):
|
||||
"""Implements synapse.events.SpamChecker.check_event_for_spam
|
||||
"""
|
||||
return False
|
||||
|
||||
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
|
||||
room_id, new_room, published_room=False):
|
||||
"""Implements synapse.events.SpamChecker.user_may_invite
|
||||
"""
|
||||
if self.can_only_invite_during_room_creation and not new_room:
|
||||
return False
|
||||
|
||||
if not self.can_invite_by_third_party_id and third_party_invite:
|
||||
return False
|
||||
|
||||
# This is a third party invite (without a bound mxid), so unless we have
|
||||
# banned all third party invites (above) we allow it.
|
||||
if not invitee_userid:
|
||||
return True
|
||||
|
||||
inviter_domain = self._get_domain_from_id(inviter_userid)
|
||||
invitee_domain = self._get_domain_from_id(invitee_userid)
|
||||
|
||||
if inviter_domain not in self.domain_mapping:
|
||||
return self.default
|
||||
|
||||
if (
|
||||
published_room and
|
||||
invitee_domain in self.domains_prevented_from_being_invited_to_published_rooms
|
||||
):
|
||||
return False
|
||||
|
||||
return invitee_domain in self.domain_mapping[inviter_domain]
|
||||
|
||||
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
|
||||
cloning):
|
||||
"""Implements synapse.events.SpamChecker.user_may_create_room
|
||||
"""
|
||||
|
||||
if cloning:
|
||||
return True
|
||||
|
||||
if not self.can_invite_by_third_party_id and third_party_invite_list:
|
||||
return False
|
||||
|
||||
number_of_invites = len(invite_list) + len(third_party_invite_list)
|
||||
|
||||
if self.can_only_create_one_to_one_rooms and number_of_invites != 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def user_may_create_room_alias(self, userid, room_alias):
|
||||
"""Implements synapse.events.SpamChecker.user_may_create_room_alias
|
||||
"""
|
||||
return True
|
||||
|
||||
def user_may_publish_room(self, userid, room_id):
|
||||
"""Implements synapse.events.SpamChecker.user_may_publish_room
|
||||
"""
|
||||
return True
|
||||
|
||||
def user_may_join_room(self, userid, room_id, is_invited):
|
||||
"""Implements synapse.events.SpamChecker.user_may_join_room
|
||||
"""
|
||||
if self.can_only_join_rooms_with_invite and not is_invited:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config):
|
||||
"""Implements synapse.events.SpamChecker.parse_config
|
||||
"""
|
||||
if "default" in config:
|
||||
return config
|
||||
else:
|
||||
raise ConfigError("No default set for spam_config DomainRuleChecker")
|
||||
|
||||
@staticmethod
|
||||
def _get_domain_from_id(mxid):
|
||||
"""Parses a string and returns the domain part of the mxid.
|
||||
|
||||
Args:
|
||||
mxid (str): a valid mxid
|
||||
|
||||
Returns:
|
||||
str: the domain part of the mxid
|
||||
|
||||
"""
|
||||
idx = mxid.find(":")
|
||||
if idx == -1:
|
||||
raise Exception("Invalid ID: %r" % (mxid,))
|
||||
return mxid[idx + 1:]
|
||||
@@ -65,6 +65,7 @@ from synapse.handlers.groups_local import GroupsLocalHandler
|
||||
from synapse.handlers.initial_sync import InitialSyncHandler
|
||||
from synapse.handlers.message import EventCreationHandler, MessageHandler
|
||||
from synapse.handlers.pagination import PaginationHandler
|
||||
from synapse.handlers.password_policy import PasswordPolicyHandler
|
||||
from synapse.handlers.presence import PresenceHandler
|
||||
from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler
|
||||
from synapse.handlers.read_marker import ReadMarkerHandler
|
||||
@@ -193,6 +194,7 @@ class HomeServer(object):
|
||||
'registration_handler',
|
||||
'account_validity_handler',
|
||||
'event_client_serializer',
|
||||
'password_policy_handler',
|
||||
]
|
||||
|
||||
REQUIRED_ON_MASTER_STARTUP = [
|
||||
@@ -529,6 +531,9 @@ class HomeServer(object):
|
||||
def build_event_client_serializer(self):
|
||||
return EventClientSerializer(self)
|
||||
|
||||
def build_password_policy_handler(self):
|
||||
return PasswordPolicyHandler(self)
|
||||
|
||||
def remove_pusher(self, app_id, push_key, user_id):
|
||||
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)
|
||||
|
||||
|
||||
@@ -299,12 +299,12 @@ class SQLBaseStore(object):
|
||||
|
||||
def select_users_with_no_expiration_date_txn(txn):
|
||||
"""Retrieves the list of registered users with no expiration date from the
|
||||
database.
|
||||
database, filtering out deactivated users.
|
||||
"""
|
||||
sql = (
|
||||
"SELECT users.name FROM users"
|
||||
" LEFT JOIN account_validity ON (users.name = account_validity.user_id)"
|
||||
" WHERE account_validity.user_id is NULL;"
|
||||
" WHERE account_validity.user_id is NULL AND users.deactivated = 0;"
|
||||
)
|
||||
txn.execute(sql, [])
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def _make_exclusive_regex(services_cache):
|
||||
exclusive_user_regexes = [
|
||||
regex.pattern
|
||||
for service in services_cache
|
||||
for regex in service.get_exlusive_user_regexes()
|
||||
for regex in service.get_exclusive_user_regexes()
|
||||
]
|
||||
if exclusive_user_regexes:
|
||||
exclusive_user_regex = "|".join("(" + r + ")" for r in exclusive_user_regexes)
|
||||
|
||||
@@ -1405,6 +1405,9 @@ class EventsStore(
|
||||
elif event.type == EventTypes.GuestAccess:
|
||||
# Insert into the event_search table.
|
||||
self._store_guest_access_txn(txn, event)
|
||||
elif event.type == EventTypes.Retention:
|
||||
# Update the room_retention table.
|
||||
self._store_retention_policy_for_room_txn(txn, event)
|
||||
|
||||
self._handle_event_relations(txn, event)
|
||||
|
||||
|
||||
@@ -255,6 +255,26 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
# didn't have the redacted event at the time, so we recheck on read
|
||||
# instead.
|
||||
if not allow_rejected and entry.event.type == EventTypes.Redaction:
|
||||
orig_event_info = yield self._simple_select_one(
|
||||
table="events",
|
||||
keyvalues={"event_id": entry.event.redacts},
|
||||
retcols=["sender", "room_id", "type"],
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
if not orig_event_info:
|
||||
# We don't have the event that is being redacted, so we
|
||||
# assume that the event isn't authorized for now. (If we
|
||||
# later receive the event, then we will always redact
|
||||
# it anyway, since we have this redaction)
|
||||
continue
|
||||
|
||||
if orig_event_info["room_id"] != entry.event.room_id:
|
||||
# Don't process redactions if the redacted event doesn't belong to the
|
||||
# redaction's room.
|
||||
logger.info("Ignoring redation in another room.")
|
||||
continue
|
||||
|
||||
if entry.event.internal_metadata.need_to_check_redaction():
|
||||
# XXX: we need to avoid calling get_event here.
|
||||
#
|
||||
@@ -277,27 +297,13 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
# 2. have _get_event_from_row just call the first half of
|
||||
# that
|
||||
|
||||
orig_sender = yield self._simple_select_one_onecol(
|
||||
table="events",
|
||||
keyvalues={"event_id": entry.event.redacts},
|
||||
retcol="sender",
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
expected_domain = get_domain_from_id(entry.event.sender)
|
||||
if (
|
||||
orig_sender
|
||||
and get_domain_from_id(orig_sender) == expected_domain
|
||||
get_domain_from_id(orig_event_info["sender"]) == expected_domain
|
||||
):
|
||||
# This redaction event is allowed. Mark as not needing a
|
||||
# recheck.
|
||||
entry.event.internal_metadata.recheck_redaction = False
|
||||
else:
|
||||
# We don't have the event that is being redacted, so we
|
||||
# assume that the event isn't authorized for now. (If we
|
||||
# later receive the event, then we will always redact
|
||||
# it anyway, since we have this redaction)
|
||||
continue
|
||||
|
||||
if allow_rejected or not entry.event.rejected_reason:
|
||||
if check_redacted and entry.redacted_event:
|
||||
@@ -532,7 +538,7 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
)
|
||||
|
||||
redacted_event = None
|
||||
if redacted:
|
||||
if redacted and original_ev.type != EventTypes.Redaction:
|
||||
redacted_event = prune_event(original_ev)
|
||||
|
||||
redaction_id = yield self._simple_select_one_onecol(
|
||||
@@ -564,7 +570,16 @@ class EventsWorkerStore(SQLBaseStore):
|
||||
# recheck.
|
||||
because.internal_metadata.recheck_redaction = False
|
||||
else:
|
||||
# Senders don't match, so the event isn't actually redacted
|
||||
# Senders don't match, so the event isn't actually
|
||||
# redacted
|
||||
redacted_event = None
|
||||
|
||||
if because.room_id != original_ev.room_id:
|
||||
redacted_event = None
|
||||
else:
|
||||
# The lack of a redaction likely means that the redaction is invalid
|
||||
# and therefore not returned by get_event, so it should be safe to
|
||||
# just ignore it here.
|
||||
redacted_event = None
|
||||
|
||||
cache_entry = _EventCacheEntry(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -18,8 +19,11 @@ from twisted.internet import defer
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.storage.roommember import ProfileInfo
|
||||
|
||||
from . import background_updates
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
BATCH_SIZE = 100
|
||||
|
||||
|
||||
class ProfileWorkerStore(SQLBaseStore):
|
||||
@defer.inlineCallbacks
|
||||
@@ -61,6 +65,55 @@ class ProfileWorkerStore(SQLBaseStore):
|
||||
desc="get_profile_avatar_url",
|
||||
)
|
||||
|
||||
def get_latest_profile_replication_batch_number(self):
|
||||
def f(txn):
|
||||
txn.execute("SELECT MAX(batch) as maxbatch FROM profiles")
|
||||
rows = self.cursor_to_dict(txn)
|
||||
return rows[0]['maxbatch']
|
||||
return self.runInteraction(
|
||||
"get_latest_profile_replication_batch_number", f,
|
||||
)
|
||||
|
||||
def get_profile_batch(self, batchnum):
|
||||
return self._simple_select_list(
|
||||
table="profiles",
|
||||
keyvalues={
|
||||
"batch": batchnum,
|
||||
},
|
||||
retcols=("user_id", "displayname", "avatar_url", "active"),
|
||||
desc="get_profile_batch",
|
||||
)
|
||||
|
||||
def assign_profile_batch(self):
|
||||
def f(txn):
|
||||
sql = (
|
||||
"UPDATE profiles SET batch = "
|
||||
"(SELECT COALESCE(MAX(batch), -1) + 1 FROM profiles) "
|
||||
"WHERE user_id in ("
|
||||
" SELECT user_id FROM profiles WHERE batch is NULL limit ?"
|
||||
")"
|
||||
)
|
||||
txn.execute(sql, (BATCH_SIZE,))
|
||||
return txn.rowcount
|
||||
return self.runInteraction("assign_profile_batch", f)
|
||||
|
||||
def get_replication_hosts(self):
|
||||
def f(txn):
|
||||
txn.execute("SELECT host, last_synced_batch FROM profile_replication_status")
|
||||
rows = self.cursor_to_dict(txn)
|
||||
return {r['host']: r['last_synced_batch'] for r in rows}
|
||||
return self.runInteraction("get_replication_hosts", f)
|
||||
|
||||
def update_replication_batch_for_host(self, host, last_synced_batch):
|
||||
return self._simple_upsert(
|
||||
table="profile_replication_status",
|
||||
keyvalues={"host": host},
|
||||
values={
|
||||
"last_synced_batch": last_synced_batch,
|
||||
},
|
||||
desc="update_replication_batch_for_host",
|
||||
)
|
||||
|
||||
def get_from_remote_profile_cache(self, user_id):
|
||||
return self._simple_select_one(
|
||||
table="remote_profile_cache",
|
||||
@@ -70,29 +123,62 @@ class ProfileWorkerStore(SQLBaseStore):
|
||||
desc="get_from_remote_profile_cache",
|
||||
)
|
||||
|
||||
def create_profile(self, user_localpart):
|
||||
return self._simple_insert(
|
||||
table="profiles", values={"user_id": user_localpart}, desc="create_profile"
|
||||
)
|
||||
|
||||
def set_profile_displayname(self, user_localpart, new_displayname):
|
||||
return self._simple_update_one(
|
||||
def set_profile_displayname(self, user_localpart, new_displayname, batchnum):
|
||||
return self._simple_upsert(
|
||||
table="profiles",
|
||||
keyvalues={"user_id": user_localpart},
|
||||
updatevalues={"displayname": new_displayname},
|
||||
values={
|
||||
"displayname": new_displayname,
|
||||
"batch": batchnum,
|
||||
},
|
||||
desc="set_profile_displayname",
|
||||
lock=False # we can do this because user_id has a unique index
|
||||
)
|
||||
|
||||
def set_profile_avatar_url(self, user_localpart, new_avatar_url):
|
||||
return self._simple_update_one(
|
||||
def set_profile_avatar_url(self, user_localpart, new_avatar_url, batchnum):
|
||||
return self._simple_upsert(
|
||||
table="profiles",
|
||||
keyvalues={"user_id": user_localpart},
|
||||
updatevalues={"avatar_url": new_avatar_url},
|
||||
values={
|
||||
"avatar_url": new_avatar_url,
|
||||
"batch": batchnum,
|
||||
},
|
||||
desc="set_profile_avatar_url",
|
||||
lock=False # we can do this because user_id has a unique index
|
||||
)
|
||||
|
||||
def set_profile_active(self, user_localpart, active, hide, batchnum):
|
||||
values = {
|
||||
"active": int(active),
|
||||
"batch": batchnum,
|
||||
}
|
||||
if not active and not hide:
|
||||
# we are deactivating for real (not in hide mode)
|
||||
# so clear the profile.
|
||||
values["avatar_url"] = None
|
||||
values["displayname"] = None
|
||||
return self._simple_upsert(
|
||||
table="profiles",
|
||||
keyvalues={"user_id": user_localpart},
|
||||
values=values,
|
||||
desc="set_profile_active",
|
||||
lock=False # we can do this because user_id has a unique index
|
||||
)
|
||||
|
||||
|
||||
class ProfileStore(ProfileWorkerStore):
|
||||
class ProfileStore(ProfileWorkerStore, background_updates.BackgroundUpdateStore):
|
||||
def __init__(self, db_conn, hs):
|
||||
|
||||
super(ProfileStore, self).__init__(db_conn, hs)
|
||||
|
||||
self.register_background_index_update(
|
||||
"profile_replication_status_host_index",
|
||||
index_name="profile_replication_status_idx",
|
||||
table="profile_replication_status",
|
||||
columns=["host"],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
def add_remote_profile_cache(self, user_id, displayname, avatar_url):
|
||||
"""Ensure we are caching the remote user's profiles.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from six import iterkeys
|
||||
@@ -31,6 +32,8 @@ from synapse.util.caches.descriptors import cached, cachedInlineCallbacks
|
||||
|
||||
THIRTY_MINUTES_IN_MS = 30 * 60 * 1000
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RegistrationWorkerStore(SQLBaseStore):
|
||||
def __init__(self, db_conn, hs):
|
||||
@@ -248,6 +251,20 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||
desc="set_renewal_mail_status",
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def delete_account_validity_for_user(self, user_id):
|
||||
"""Deletes the entry for the given user in the account validity table, removing
|
||||
their expiration date and renewal token.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to remove from the account validity table.
|
||||
"""
|
||||
yield self._simple_delete_one(
|
||||
table="account_validity",
|
||||
keyvalues={"user_id": user_id},
|
||||
desc="delete_account_validity_for_user",
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_server_admin(self, user):
|
||||
res = yield self._simple_select_one_onecol(
|
||||
@@ -598,11 +615,77 @@ class RegistrationStore(
|
||||
"user_threepids_grandfather", self._bg_user_threepids_grandfather,
|
||||
)
|
||||
|
||||
self.register_background_update_handler(
|
||||
"users_set_deactivated_flag", self._backgroud_update_set_deactivated_flag,
|
||||
)
|
||||
|
||||
# Create a background job for culling expired 3PID validity tokens
|
||||
hs.get_clock().looping_call(
|
||||
self.cull_expired_threepid_validation_tokens, THIRTY_MINUTES_IN_MS,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _backgroud_update_set_deactivated_flag(self, progress, batch_size):
|
||||
"""Retrieves a list of all deactivated users and sets the 'deactivated' flag to 1
|
||||
for each of them.
|
||||
"""
|
||||
|
||||
last_user = progress.get("user_id", "")
|
||||
|
||||
def _backgroud_update_set_deactivated_flag_txn(txn):
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT
|
||||
users.name,
|
||||
COUNT(access_tokens.token) AS count_tokens,
|
||||
COUNT(user_threepids.address) AS count_threepids
|
||||
FROM users
|
||||
LEFT JOIN access_tokens ON (access_tokens.user_id = users.name)
|
||||
LEFT JOIN user_threepids ON (user_threepids.user_id = users.name)
|
||||
WHERE (users.password_hash IS NULL OR users.password_hash = '')
|
||||
AND (users.appservice_id IS NULL OR users.appservice_id = '')
|
||||
AND users.is_guest = 0
|
||||
AND users.name > ?
|
||||
GROUP BY users.name
|
||||
ORDER BY users.name ASC
|
||||
LIMIT ?;
|
||||
""",
|
||||
(last_user, batch_size),
|
||||
)
|
||||
|
||||
rows = self.cursor_to_dict(txn)
|
||||
|
||||
if not rows:
|
||||
return True
|
||||
|
||||
rows_processed_nb = 0
|
||||
|
||||
for user in rows:
|
||||
if not user["count_tokens"] and not user["count_threepids"]:
|
||||
self.set_user_deactivated_status_txn(txn, user["name"], True)
|
||||
rows_processed_nb += 1
|
||||
|
||||
logger.info("Marked %d rows as deactivated", rows_processed_nb)
|
||||
|
||||
self._background_update_progress_txn(
|
||||
txn, "users_set_deactivated_flag", {"user_id": rows[-1]["name"]}
|
||||
)
|
||||
|
||||
if batch_size > len(rows):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
end = yield self.runInteraction(
|
||||
"users_set_deactivated_flag",
|
||||
_backgroud_update_set_deactivated_flag_txn,
|
||||
)
|
||||
|
||||
if end:
|
||||
yield self._end_background_update("users_set_deactivated_flag")
|
||||
|
||||
defer.returnValue(batch_size)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def add_access_token_to_user(self, user_id, token, device_id=None):
|
||||
"""Adds an access token for the given user.
|
||||
@@ -1268,3 +1351,50 @@ class RegistrationStore(
|
||||
"delete_threepid_session",
|
||||
delete_threepid_session_txn,
|
||||
)
|
||||
|
||||
def set_user_deactivated_status_txn(self, txn, user_id, deactivated):
|
||||
self._simple_update_one_txn(
|
||||
txn=txn,
|
||||
table="users",
|
||||
keyvalues={"name": user_id},
|
||||
updatevalues={"deactivated": 1 if deactivated else 0},
|
||||
)
|
||||
self._invalidate_cache_and_stream(
|
||||
txn, self.get_user_deactivated_status, (user_id,),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_user_deactivated_status(self, user_id, deactivated):
|
||||
"""Set the `deactivated` property for the provided user to the provided value.
|
||||
|
||||
Args:
|
||||
user_id (str): The ID of the user to set the status for.
|
||||
deactivated (bool): The value to set for `deactivated`.
|
||||
"""
|
||||
|
||||
yield self.runInteraction(
|
||||
"set_user_deactivated_status",
|
||||
self.set_user_deactivated_status_txn,
|
||||
user_id, deactivated,
|
||||
)
|
||||
|
||||
@cachedInlineCallbacks()
|
||||
def get_user_deactivated_status(self, user_id):
|
||||
"""Retrieve the value for the `deactivated` property for the provided user.
|
||||
|
||||
Args:
|
||||
user_id (str): The ID of the user to retrieve the status for.
|
||||
|
||||
Returns:
|
||||
defer.Deferred(bool): The requested value.
|
||||
"""
|
||||
|
||||
res = yield self._simple_select_one_onecol(
|
||||
table="users",
|
||||
keyvalues={"name": user_id},
|
||||
retcol="deactivated",
|
||||
desc="get_user_deactivated_status",
|
||||
)
|
||||
|
||||
# Convert the integer into a boolean.
|
||||
defer.returnValue(res == 1)
|
||||
|
||||
@@ -17,10 +17,13 @@ import collections
|
||||
import logging
|
||||
import re
|
||||
|
||||
from six import integer_types
|
||||
|
||||
from canonicaljson import json
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.storage._base import SQLBaseStore
|
||||
from synapse.storage.search import SearchStore
|
||||
@@ -171,6 +174,24 @@ class RoomWorkerStore(SQLBaseStore):
|
||||
desc="is_room_blocked",
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_room_published(self, room_id):
|
||||
"""Check whether a room has been published in the local public room
|
||||
directory.
|
||||
|
||||
Args:
|
||||
room_id (str)
|
||||
Returns:
|
||||
bool: Whether the room is currently published in the room directory
|
||||
"""
|
||||
# Get room information
|
||||
room_info = yield self.get_room(room_id)
|
||||
if not room_info:
|
||||
defer.returnValue(False)
|
||||
|
||||
# Check the is_public value
|
||||
defer.returnValue(room_info.get("is_public", False))
|
||||
|
||||
@cachedInlineCallbacks(max_entries=10000)
|
||||
def get_ratelimit_for_user(self, user_id):
|
||||
"""Check if there are any overrides for ratelimiting for the given
|
||||
@@ -204,6 +225,85 @@ class RoomWorkerStore(SQLBaseStore):
|
||||
|
||||
|
||||
class RoomStore(RoomWorkerStore, SearchStore):
|
||||
def __init__(self, db_conn, hs):
|
||||
super(RoomStore, self).__init__(db_conn, hs)
|
||||
|
||||
self.config = hs.config
|
||||
|
||||
self.register_background_update_handler(
|
||||
"insert_room_retention", self._background_insert_retention,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _background_insert_retention(self, progress, batch_size):
|
||||
"""Retrieves a list of all rooms within a range and inserts an entry for each of
|
||||
them into the room_retention table.
|
||||
NULLs the property's columns if missing from the retention event in the room's
|
||||
state (or NULLs all of them if there's no retention event in the room's state),
|
||||
so that we fall back to the server's retention policy.
|
||||
"""
|
||||
|
||||
last_room = progress.get("room_id", "")
|
||||
|
||||
def _background_insert_retention_txn(txn):
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT state.room_id, state.event_id, events.json
|
||||
FROM current_state_events as state
|
||||
LEFT JOIN event_json AS events ON (state.event_id = events.event_id)
|
||||
WHERE state.room_id > ? AND state.type = '%s'
|
||||
ORDER BY state.room_id ASC
|
||||
LIMIT ?;
|
||||
""" % EventTypes.Retention,
|
||||
(last_room, batch_size)
|
||||
)
|
||||
|
||||
rows = self.cursor_to_dict(txn)
|
||||
|
||||
if not rows:
|
||||
return True
|
||||
|
||||
for row in rows:
|
||||
if not row["json"]:
|
||||
retention_policy = {}
|
||||
else:
|
||||
ev = json.loads(row["json"])
|
||||
retention_policy = json.dumps(ev["content"])
|
||||
|
||||
self._simple_insert_txn(
|
||||
txn=txn,
|
||||
table="room_retention",
|
||||
values={
|
||||
"room_id": row["room_id"],
|
||||
"event_id": row["event_id"],
|
||||
"min_lifetime": retention_policy.get("min_lifetime"),
|
||||
"max_lifetime": retention_policy.get("max_lifetime"),
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("Inserted %d rows into room_retention", len(rows))
|
||||
|
||||
self._background_update_progress_txn(
|
||||
txn, "insert_room_retention", {
|
||||
"room_id": rows[-1]["room_id"],
|
||||
}
|
||||
)
|
||||
|
||||
if batch_size > len(rows):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
end = yield self.runInteraction(
|
||||
"insert_room_retention",
|
||||
_background_insert_retention_txn,
|
||||
)
|
||||
|
||||
if end:
|
||||
yield self._end_background_update("insert_room_retention")
|
||||
|
||||
defer.returnValue(batch_size)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def store_room(self, room_id, room_creator_user_id, is_public):
|
||||
"""Stores a room.
|
||||
@@ -439,6 +539,37 @@ class RoomStore(RoomWorkerStore, SearchStore):
|
||||
)
|
||||
txn.execute(sql, (event.event_id, event.room_id, event.content[key]))
|
||||
|
||||
def _store_retention_policy_for_room_txn(self, txn, event):
|
||||
if (
|
||||
hasattr(event, "content")
|
||||
and ("min_lifetime" in event.content or "max_lifetime" in event.content)
|
||||
):
|
||||
if (
|
||||
("min_lifetime" in event.content and not isinstance(
|
||||
event.content.get("min_lifetime"), integer_types
|
||||
))
|
||||
or ("max_lifetime" in event.content and not isinstance(
|
||||
event.content.get("max_lifetime"), integer_types
|
||||
))
|
||||
):
|
||||
# Ignore the event if one of the value isn't an integer.
|
||||
return
|
||||
|
||||
self._simple_insert_txn(
|
||||
txn=txn,
|
||||
table="room_retention",
|
||||
values={
|
||||
"room_id": event.room_id,
|
||||
"event_id": event.event_id,
|
||||
"min_lifetime": event.content.get("min_lifetime"),
|
||||
"max_lifetime": event.content.get("max_lifetime"),
|
||||
},
|
||||
)
|
||||
|
||||
self._invalidate_cache_and_stream(
|
||||
txn, self.get_retention_policy_for_room, (event.room_id,)
|
||||
)
|
||||
|
||||
def add_event_report(
|
||||
self, room_id, event_id, user_id, reason, content, received_ts
|
||||
):
|
||||
@@ -620,3 +751,142 @@ class RoomStore(RoomWorkerStore, SearchStore):
|
||||
remote_media_mxcs.append((hostname, media_id))
|
||||
|
||||
return local_media_mxcs, remote_media_mxcs
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rooms_for_retention_period_in_range(self, min_ms, max_ms, include_null=False):
|
||||
"""Retrieves all of the rooms within the given retention range.
|
||||
|
||||
Optionally includes the rooms which don't have a retention policy.
|
||||
|
||||
Args:
|
||||
min_ms (int|None): Duration in milliseconds that define the lower limit of
|
||||
the range to handle (exclusive). If None, doesn't set a lower limit.
|
||||
max_ms (int|None): Duration in milliseconds that define the upper limit of
|
||||
the range to handle (inclusive). If None, doesn't set an upper limit.
|
||||
include_null (bool): Whether to include rooms which retention policy is NULL
|
||||
in the returned set.
|
||||
|
||||
Returns:
|
||||
dict[str, dict]: The rooms within this range, along with their retention
|
||||
policy. The key is "room_id", and maps to a dict describing the retention
|
||||
policy associated with this room ID. The keys for this nested dict are
|
||||
"min_lifetime" (int|None), and "max_lifetime" (int|None).
|
||||
"""
|
||||
|
||||
def get_rooms_for_retention_period_in_range_txn(txn):
|
||||
range_conditions = []
|
||||
args = []
|
||||
|
||||
if min_ms is not None:
|
||||
range_conditions.append("max_lifetime > ?")
|
||||
args.append(min_ms)
|
||||
|
||||
if max_ms is not None:
|
||||
range_conditions.append("max_lifetime <= ?")
|
||||
args.append(max_ms)
|
||||
|
||||
# Do a first query which will retrieve the rooms that have a retention policy
|
||||
# in their current state.
|
||||
sql = """
|
||||
SELECT room_id, min_lifetime, max_lifetime FROM room_retention
|
||||
INNER JOIN current_state_events USING (event_id, room_id)
|
||||
"""
|
||||
|
||||
if len(range_conditions):
|
||||
sql += " WHERE (" + " AND ".join(range_conditions) + ")"
|
||||
|
||||
if include_null:
|
||||
sql += " OR max_lifetime IS NULL"
|
||||
|
||||
txn.execute(sql, args)
|
||||
|
||||
rows = self.cursor_to_dict(txn)
|
||||
rooms_dict = {}
|
||||
|
||||
for row in rows:
|
||||
rooms_dict[row["room_id"]] = {
|
||||
"min_lifetime": row["min_lifetime"],
|
||||
"max_lifetime": row["max_lifetime"],
|
||||
}
|
||||
|
||||
if include_null:
|
||||
# If required, do a second query that retrieves all of the rooms we know
|
||||
# of so we can handle rooms with no retention policy.
|
||||
sql = "SELECT DISTINCT room_id FROM current_state_events"
|
||||
|
||||
txn.execute(sql)
|
||||
|
||||
rows = self.cursor_to_dict(txn)
|
||||
|
||||
# If a room isn't already in the dict (i.e. it doesn't have a retention
|
||||
# policy in its state), add it with a null policy.
|
||||
for row in rows:
|
||||
if row["room_id"] not in rooms_dict:
|
||||
rooms_dict[row["room_id"]] = {
|
||||
"min_lifetime": None,
|
||||
"max_lifetime": None,
|
||||
}
|
||||
|
||||
return rooms_dict
|
||||
|
||||
rooms = yield self.runInteraction(
|
||||
"get_rooms_for_retention_period_in_range",
|
||||
get_rooms_for_retention_period_in_range_txn,
|
||||
)
|
||||
|
||||
defer.returnValue(rooms)
|
||||
|
||||
@cachedInlineCallbacks()
|
||||
def get_retention_policy_for_room(self, room_id):
|
||||
"""Get the retention policy for a given room.
|
||||
|
||||
If no retention policy has been found for this room, returns a policy defined
|
||||
by the configured default policy (which has None as both the 'min_lifetime' and
|
||||
the 'max_lifetime' if no default policy has been defined in the server's
|
||||
configuration).
|
||||
|
||||
Args:
|
||||
room_id (str): The ID of the room to get the retention policy of.
|
||||
|
||||
Returns:
|
||||
dict[int, int]: "min_lifetime" and "max_lifetime" for this room.
|
||||
"""
|
||||
|
||||
def get_retention_policy_for_room_txn(txn):
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT min_lifetime, max_lifetime FROM room_retention
|
||||
INNER JOIN current_state_events USING (event_id, room_id)
|
||||
WHERE room_id = ?;
|
||||
""",
|
||||
(room_id,)
|
||||
)
|
||||
|
||||
return self.cursor_to_dict(txn)
|
||||
|
||||
ret = yield self.runInteraction(
|
||||
"get_retention_policy_for_room",
|
||||
get_retention_policy_for_room_txn,
|
||||
)
|
||||
|
||||
# If we don't know this room ID, ret will be None, in this case return the default
|
||||
# policy.
|
||||
if not ret:
|
||||
defer.returnValue({
|
||||
"min_lifetime": self.config.retention_default_min_lifetime,
|
||||
"max_lifetime": self.config.retention_default_max_lifetime,
|
||||
})
|
||||
|
||||
row = ret[0]
|
||||
|
||||
# If one of the room's policy's attributes isn't defined, use the matching
|
||||
# attribute from the default policy.
|
||||
# The default values will be None if no default policy has been defined, or if one
|
||||
# of the attributes is missing from the default policy.
|
||||
if row["min_lifetime"] is None:
|
||||
row["min_lifetime"] = self.config.retention_default_min_lifetime
|
||||
|
||||
if row["max_lifetime"] is None:
|
||||
row["max_lifetime"] = self.config.retention_default_max_lifetime
|
||||
|
||||
defer.returnValue(row)
|
||||
|
||||
36
synapse/storage/schema/delta/48/profiles_batch.sql
Normal file
36
synapse/storage/schema/delta/48/profiles_batch.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
/* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Add a batch number to track changes to profiles and the
|
||||
* order they're made in so we can replicate user profiles
|
||||
* to other hosts as they change
|
||||
*/
|
||||
ALTER TABLE profiles ADD COLUMN batch BIGINT DEFAULT NULL;
|
||||
|
||||
/*
|
||||
* Index on the batch number so we can get profiles
|
||||
* by their batch
|
||||
*/
|
||||
CREATE INDEX profiles_batch_idx ON profiles(batch);
|
||||
|
||||
/*
|
||||
* A table to track what batch of user profiles has been
|
||||
* synced to what profile replication target.
|
||||
*/
|
||||
CREATE TABLE profile_replication_status (
|
||||
host TEXT NOT NULL,
|
||||
last_synced_batch BIGINT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
/* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* A flag saying whether the user owning the profile has been deactivated
|
||||
* This really belongs on the users table, not here, but the users table
|
||||
* stores users by their full user_id and profiles stores them by localpart,
|
||||
* so we can't easily join between the two tables. Plus, the batch number
|
||||
* realy ought to represent data in this table that has changed.
|
||||
*/
|
||||
ALTER TABLE profiles ADD COLUMN active SMALLINT DEFAULT 1 NOT NULL;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
INSERT INTO background_updates (update_name, progress_json) VALUES
|
||||
('profile_replication_status_host_index', '{}');
|
||||
33
synapse/storage/schema/delta/55/room_retention.sql
Normal file
33
synapse/storage/schema/delta/55/room_retention.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
-- Tracks the retention policy of a room.
|
||||
-- A NULL max_lifetime or min_lifetime means that the matching property is not defined in
|
||||
-- the room's retention policy state event.
|
||||
-- If a room doesn't have a retention policy state event in its state, both max_lifetime
|
||||
-- and min_lifetime are NULL.
|
||||
CREATE TABLE IF NOT EXISTS room_retention(
|
||||
room_id TEXT,
|
||||
event_id TEXT,
|
||||
min_lifetime BIGINT,
|
||||
max_lifetime BIGINT,
|
||||
|
||||
PRIMARY KEY(room_id, event_id)
|
||||
);
|
||||
|
||||
CREATE INDEX room_retention_max_lifetime_idx on room_retention(max_lifetime);
|
||||
|
||||
INSERT INTO background_updates (update_name, progress_json) VALUES
|
||||
('insert_room_retention', '{}');
|
||||
19
synapse/storage/schema/delta/55/users_alter_deactivated.sql
Normal file
19
synapse/storage/schema/delta/55/users_alter_deactivated.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
/* Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
ALTER TABLE users ADD deactivated SMALLINT DEFAULT 0 NOT NULL;
|
||||
|
||||
INSERT INTO background_updates (update_name, progress_json) VALUES
|
||||
('users_set_deactivated_flag', '{}');
|
||||
@@ -667,10 +667,19 @@ CREATE TABLE presence_stream (
|
||||
|
||||
|
||||
|
||||
CREATE TABLE profile_replication_status (
|
||||
host text NOT NULL,
|
||||
last_synced_batch bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE profiles (
|
||||
user_id text NOT NULL,
|
||||
displayname text,
|
||||
avatar_url text
|
||||
avatar_url text,
|
||||
batch bigint,
|
||||
active smallint DEFAULT 1 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -1842,6 +1851,10 @@ CREATE INDEX presence_stream_user_id ON presence_stream USING btree (user_id);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX profiles_batch_idx ON profiles USING btree (batch);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX public_room_index ON rooms USING btree (is_public);
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ CREATE TABLE presence_allow_inbound( observed_user_id TEXT NOT NULL, observer_us
|
||||
CREATE TABLE users( name TEXT, password_hash TEXT, creation_ts BIGINT, admin SMALLINT DEFAULT 0 NOT NULL, upgrade_ts BIGINT, is_guest SMALLINT DEFAULT 0 NOT NULL, appservice_id TEXT, consent_version TEXT, consent_server_notice_sent TEXT, user_type TEXT DEFAULT NULL, UNIQUE(name) );
|
||||
CREATE TABLE access_tokens( id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL, last_used BIGINT, UNIQUE(token) );
|
||||
CREATE TABLE user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL );
|
||||
CREATE TABLE profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, UNIQUE(user_id) );
|
||||
CREATE TABLE profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, batch BIGINT DEFAULT NULL, active SMALLINT DEFAULT 1 NOT NULL, UNIQUE(user_id) );
|
||||
CREATE TABLE received_transactions( transaction_id TEXT, origin TEXT, ts BIGINT, response_code INTEGER, response_json bytea, has_been_referenced smallint default 0, UNIQUE (transaction_id, origin) );
|
||||
CREATE TABLE destinations( destination TEXT PRIMARY KEY, retry_last_ts BIGINT, retry_interval INTEGER );
|
||||
CREATE TABLE events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed BOOL NOT NULL, outlier BOOL NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url BOOLEAN, UNIQUE (event_id) );
|
||||
@@ -208,6 +208,8 @@ CREATE INDEX group_users_u_idx ON group_users(user_id);
|
||||
CREATE INDEX group_invites_u_idx ON group_invites(user_id);
|
||||
CREATE UNIQUE INDEX group_rooms_g_idx ON group_rooms(group_id, room_id);
|
||||
CREATE INDEX group_rooms_r_idx ON group_rooms(room_id);
|
||||
CREATE INDEX profiles_batch_idx ON profiles(batch);
|
||||
CREATE TABLE profile_replication_status ( host TEXT NOT NULL, last_synced_batch BIGINT NOT NULL );
|
||||
CREATE TABLE user_daily_visits ( user_id TEXT NOT NULL, device_id TEXT, timestamp BIGINT NOT NULL );
|
||||
CREATE INDEX user_daily_visits_uts_idx ON user_daily_visits(user_id, timestamp);
|
||||
CREATE INDEX user_daily_visits_ts_idx ON user_daily_visits(timestamp);
|
||||
|
||||
14
synapse/third_party_rules/__init__.py
Normal file
14
synapse/third_party_rules/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user