Merge remote-tracking branch 'origin/develop' into matrix-org-hotfixes
This commit is contained in:
commit
7467738834
32
INSTALL.md
32
INSTALL.md
|
@ -257,18 +257,40 @@ https://github.com/spantaleev/matrix-docker-ansible-deploy
|
|||
#### Matrix.org packages
|
||||
|
||||
Matrix.org provides Debian/Ubuntu packages of the latest stable version of
|
||||
Synapse via https://matrix.org/packages/debian/. To use them:
|
||||
Synapse via https://packages.matrix.org/debian/. To use them:
|
||||
|
||||
For Debian 9 (Stretch), Ubuntu 16.04 (Xenial), and later:
|
||||
|
||||
```
|
||||
sudo apt install -y lsb-release curl apt-transport-https
|
||||
echo "deb https://matrix.org/packages/debian `lsb_release -cs` main" |
|
||||
sudo apt install -y lsb-release wget apt-transport-https
|
||||
sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" |
|
||||
sudo tee /etc/apt/sources.list.d/matrix-org.list
|
||||
curl "https://matrix.org/packages/debian/repo-key.asc" |
|
||||
sudo apt-key add -
|
||||
sudo apt update
|
||||
sudo apt install matrix-synapse-py3
|
||||
```
|
||||
|
||||
For Debian 8 (Jessie):
|
||||
|
||||
```
|
||||
sudo apt install -y lsb-release wget apt-transport-https
|
||||
sudo wget -O /etc/apt/trusted.gpg.d/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
|
||||
echo "deb [signed-by=5586CCC0CBBBEFC7A25811ADF473DD4473365DE1] https://packages.matrix.org/debian/ $(lsb_release -cs) main" |
|
||||
sudo tee /etc/apt/sources.list.d/matrix-org.list
|
||||
sudo apt update
|
||||
sudo apt install matrix-synapse-py3
|
||||
```
|
||||
|
||||
The fingerprint of the repository signing key is AAF9AE843A7584B5A3E4CD2BCF45A512DE2DA058.
|
||||
|
||||
**Note**: if you followed a previous version of these instructions which
|
||||
recommended using `apt-key add` to add an old key from
|
||||
`https://matrix.org/packages/debian/`, you should note that this key has been
|
||||
revoked. You should remove the old key with `sudo apt-key remove
|
||||
C35EB17E1EAE708E6603A9B3AD0592FE47F0DF61`, and follow the above instructions to
|
||||
update your configuration.
|
||||
|
||||
|
||||
#### Downstream Debian/Ubuntu packages
|
||||
|
||||
For `buster` and `sid`, Synapse is available in the Debian repositories and
|
||||
|
|
|
@ -173,7 +173,7 @@ Synapse offers two database engines:
|
|||
* `PostgreSQL <https://www.postgresql.org>`_
|
||||
|
||||
By default Synapse uses SQLite in and doing so trades performance for convenience.
|
||||
SQLite is only recommended in Synapse for testing purposes or for servers with
|
||||
SQLite is only recommended in Synapse for testing purposes or for servers with
|
||||
light workloads.
|
||||
|
||||
Almost all installations should opt to use PostreSQL. Advantages include:
|
||||
|
@ -272,7 +272,7 @@ to install using pip and a virtualenv::
|
|||
|
||||
virtualenv -p python3 env
|
||||
source env/bin/activate
|
||||
python -m pip install -e .[all]
|
||||
python -m pip install --no-pep-517 -e .[all]
|
||||
|
||||
This will run a process of downloading and installing all the needed
|
||||
dependencies into a virtual env.
|
||||
|
|
1
changelog.d/4339.feature
Normal file
1
changelog.d/4339.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add systemd-python to the optional dependencies to enable logging to the systemd journal. Install with `pip install matrix-synapse[systemd]`.
|
1
changelog.d/4867.feature
Normal file
1
changelog.d/4867.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add a default .m.rule.tombstone push rule.
|
1
changelog.d/4967.feature
Normal file
1
changelog.d/4967.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Implementation of [MSC1711](https://github.com/matrix-org/matrix-doc/pull/1711) including config options for requiring valid TLS certificates for federation traffic, the ability to disable TLS validation for specific domains, and the ability to specify your own list of CA certificates.
|
1
changelog.d/4972.misc
Normal file
1
changelog.d/4972.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Reduce log level of .well-known/matrix/client responses.
|
1
changelog.d/4992.misc
Normal file
1
changelog.d/4992.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Remove a number of unused tables from the database schema.
|
1
changelog.d/5001.misc
Normal file
1
changelog.d/5001.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Clean up some code in the server-key Keyring.
|
1
changelog.d/5005.misc
Normal file
1
changelog.d/5005.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Convert SYNAPSE_NO_TLS Docker variable to boolean for user friendliness. Contributed by Gabriel Eckerson.
|
1
changelog.d/5009.bugfix
Normal file
1
changelog.d/5009.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Clients timing out/disappearing while downloading from the media repository will now no longer log a spurious "Producer was not unregistered" message.
|
1
changelog.d/5010.feature
Normal file
1
changelog.d/5010.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add config option to block users from looking up 3PIDs.
|
1
changelog.d/5020.feature
Normal file
1
changelog.d/5020.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add context to phonehome stats.
|
1
changelog.d/5024.misc
Normal file
1
changelog.d/5024.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Store the notary server name correctly in server_keys_json.
|
1
changelog.d/5027.feature
Normal file
1
changelog.d/5027.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add time-based account expiration.
|
1
changelog.d/5028.misc
Normal file
1
changelog.d/5028.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Remove a number of unused tables from the database schema.
|
1
changelog.d/5030.misc
Normal file
1
changelog.d/5030.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Rewrite Datastore.get_server_verify_keys to reduce the number of database transactions.
|
1
changelog.d/5032.bugfix
Normal file
1
changelog.d/5032.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix "cannot import name execute_batch" error with postgres.
|
1
changelog.d/5033.misc
Normal file
1
changelog.d/5033.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Remove a number of unused tables from the database schema.
|
1
changelog.d/5035.bugfix
Normal file
1
changelog.d/5035.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix disappearing exceptions in manhole.
|
1
changelog.d/5046.misc
Normal file
1
changelog.d/5046.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Remove extraneous period from copyright headers.
|
1
changelog.d/5047.feature
Normal file
1
changelog.d/5047.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add time-based account expiration.
|
1
changelog.d/5065.feature
Normal file
1
changelog.d/5065.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add support for handling /verions, /voip and /push_rules client endpoints to client_reader worker.
|
1
changelog.d/5067.misc
Normal file
1
changelog.d/5067.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Update documentation for where to get Synapse packages.
|
1
changelog.d/5071.bugfix
Normal file
1
changelog.d/5071.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Make sure we're not registering the same 3pid twice on registration.
|
1
changelog.d/5073.feature
Normal file
1
changelog.d/5073.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add time-based account expiration.
|
1
changelog.d/5077.bugfix
Normal file
1
changelog.d/5077.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Don't crash on lack of expiry templates.
|
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/5100.misc
Normal file
1
changelog.d/5100.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Improve logging when event-signature checks fail.
|
1
changelog.d/5103.bugfix
Normal file
1
changelog.d/5103.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix bug where presence updates were sent to all servers in a room when a new server joined, rather than to just the new server.
|
1
changelog.d/5116.feature
Normal file
1
changelog.d/5116.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add time-based account expiration.
|
1
changelog.d/5119.feature
Normal file
1
changelog.d/5119.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Move admin APIs to `/_synapse/admin/v1`. (The old paths are retained for backwards-compatibility, for now).
|
1
changelog.d/5120.misc
Normal file
1
changelog.d/5120.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Factor out an "assert_requester_is_admin" function.
|
1
changelog.d/5121.feature
Normal file
1
changelog.d/5121.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Implement an admin API for sending server notices. Many thanks to @krombel who provided a foundation for this work.
|
1
changelog.d/5124.bugfix
Normal file
1
changelog.d/5124.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Add some missing limitations to room alias creation.
|
File diff suppressed because one or more lines are too long
|
@ -50,7 +50,9 @@ RUN apt-get update -qq -o Acquire::Languages=none \
|
|||
debhelper \
|
||||
devscripts \
|
||||
dh-systemd \
|
||||
libsystemd-dev \
|
||||
lsb-release \
|
||||
pkg-config \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
|
|
|
@ -102,8 +102,9 @@ when ``SYNAPSE_CONFIG_PATH`` is not set.
|
|||
* ``SYNAPSE_SERVER_NAME`` (mandatory), the server public hostname.
|
||||
* ``SYNAPSE_REPORT_STATS``, (mandatory, ``yes`` or ``no``), enable anonymous
|
||||
statistics reporting back to the Matrix project which helps us to get funding.
|
||||
* ``SYNAPSE_NO_TLS``, set this variable to disable TLS in Synapse (use this if
|
||||
you run your own TLS-capable reverse proxy).
|
||||
* `SYNAPSE_NO_TLS`, (accepts `true`, `false`, `on`, `off`, `1`, `0`, `yes`, `no`]): disable
|
||||
TLS in Synapse (use this if you run your own TLS-capable reverse proxy). Defaults
|
||||
to `false` (ie, TLS is enabled by default).
|
||||
* ``SYNAPSE_ENABLE_REGISTRATION``, set this variable to enable registration on
|
||||
the Synapse instance.
|
||||
* ``SYNAPSE_ALLOW_GUEST``, set this variable to allow guest joining this server.
|
||||
|
|
|
@ -59,6 +59,18 @@ else:
|
|||
if not os.path.exists("/compiled"): os.mkdir("/compiled")
|
||||
|
||||
config_path = "/compiled/homeserver.yaml"
|
||||
|
||||
# Convert SYNAPSE_NO_TLS to boolean if exists
|
||||
if "SYNAPSE_NO_TLS" in environ:
|
||||
tlsanswerstring = str.lower(environ["SYNAPSE_NO_TLS"])
|
||||
if tlsanswerstring in ("true", "on", "1", "yes"):
|
||||
environ["SYNAPSE_NO_TLS"] = True
|
||||
else:
|
||||
if tlsanswerstring in ("false", "off", "0", "no"):
|
||||
environ["SYNAPSE_NO_TLS"] = False
|
||||
else:
|
||||
print("Environment variable \"SYNAPSE_NO_TLS\" found but value \"" + tlsanswerstring + "\" unrecognized; exiting.")
|
||||
sys.exit(2)
|
||||
|
||||
convert("/conf/homeserver.yaml", config_path, environ)
|
||||
convert("/conf/log.config", "/compiled/log.config", environ)
|
||||
|
|
|
@ -177,7 +177,6 @@ You can do this with a `.well-known` file as follows:
|
|||
on `customer.example.net:8000` it correctly handles HTTP requests with
|
||||
Host header set to `customer.example.net:8000`.
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### Synapse 0.99.0 has just been released, what do I need to do right now?
|
||||
|
|
42
docs/admin_api/account_validity.rst
Normal file
42
docs/admin_api/account_validity.rst
Normal file
|
@ -0,0 +1,42 @@
|
|||
Account validity API
|
||||
====================
|
||||
|
||||
This API allows a server administrator to manage the validity of an account. To
|
||||
use it, you must enable the account validity feature (under
|
||||
``account_validity``) in Synapse's configuration.
|
||||
|
||||
Renew account
|
||||
-------------
|
||||
|
||||
This API extends the validity of an account by as much time as configured in the
|
||||
``period`` parameter from the ``account_validity`` configuration.
|
||||
|
||||
The API is::
|
||||
|
||||
POST /_synapse/admin/v1/account_validity/validity
|
||||
|
||||
with the following body:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"user_id": "<user ID for the account to renew>",
|
||||
"expiration_ts": 0,
|
||||
"enable_renewal_emails": true
|
||||
}
|
||||
|
||||
|
||||
``expiration_ts`` is an optional parameter and overrides the expiration date,
|
||||
which otherwise defaults to now + validity period.
|
||||
|
||||
``enable_renewal_emails`` is also an optional parameter and enables/disables
|
||||
sending renewal emails to the user. Defaults to true.
|
||||
|
||||
The API returns with the new expiration date for this account, as a timestamp in
|
||||
milliseconds since epoch:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"expiration_ts": 0
|
||||
}
|
|
@ -8,7 +8,7 @@ being deleted.
|
|||
The API is:
|
||||
|
||||
```
|
||||
POST /_matrix/client/r0/admin/delete_group/<group_id>
|
||||
POST /_synapse/admin/v1/delete_group/<group_id>
|
||||
```
|
||||
|
||||
including an `access_token` of a server admin.
|
||||
|
|
|
@ -4,7 +4,7 @@ This API gets a list of known media in a room.
|
|||
|
||||
The API is:
|
||||
```
|
||||
GET /_matrix/client/r0/admin/room/<room_id>/media
|
||||
GET /_synapse/admin/v1/room/<room_id>/media
|
||||
```
|
||||
including an `access_token` of a server admin.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ paginate further back in the room from the point being purged from.
|
|||
|
||||
The API is:
|
||||
|
||||
``POST /_matrix/client/r0/admin/purge_history/<room_id>[/<event_id>]``
|
||||
``POST /_synapse/admin/v1/purge_history/<room_id>[/<event_id>]``
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
|
@ -49,7 +49,7 @@ Purge status query
|
|||
|
||||
It is possible to poll for updates on recent purges with a second API;
|
||||
|
||||
``GET /_matrix/client/r0/admin/purge_history_status/<purge_id>``
|
||||
``GET /_synapse/admin/v1/purge_history_status/<purge_id>``
|
||||
|
||||
(again, with a suitable ``access_token``). This API returns a JSON body like
|
||||
the following:
|
||||
|
|
|
@ -6,7 +6,7 @@ media.
|
|||
|
||||
The API is::
|
||||
|
||||
POST /_matrix/client/r0/admin/purge_media_cache?before_ts=<unix_timestamp_in_ms>&access_token=<access_token>
|
||||
POST /_synapse/admin/v1/purge_media_cache?before_ts=<unix_timestamp_in_ms>&access_token=<access_token>
|
||||
|
||||
{}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ is not enabled.
|
|||
|
||||
To fetch the nonce, you need to request one from the API::
|
||||
|
||||
> GET /_matrix/client/r0/admin/register
|
||||
> GET /_synapse/admin/v1/register
|
||||
|
||||
< {"nonce": "thisisanonce"}
|
||||
|
||||
|
@ -22,7 +22,7 @@ body containing the nonce, username, password, whether they are an admin
|
|||
|
||||
As an example::
|
||||
|
||||
> POST /_matrix/client/r0/admin/register
|
||||
> POST /_synapse/admin/v1/register
|
||||
> {
|
||||
"nonce": "thisisanonce",
|
||||
"username": "pepper_roni",
|
||||
|
|
48
docs/admin_api/server_notices.md
Normal file
48
docs/admin_api/server_notices.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Server Notices
|
||||
|
||||
The API to send notices is as follows:
|
||||
|
||||
```
|
||||
POST /_synapse/admin/v1/send_server_notice
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```
|
||||
PUT /_synapse/admin/v1/send_server_notice/{txnId}
|
||||
```
|
||||
|
||||
You will need to authenticate with an access token for an admin user.
|
||||
|
||||
When using the `PUT` form, retransmissions with the same transaction ID will be
|
||||
ignored in the same way as with `PUT
|
||||
/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`.
|
||||
|
||||
The request body should look something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "@target_user:server_name",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "This is my message"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can optionally include the following additional parameters:
|
||||
|
||||
* `type`: the type of event. Defaults to `m.room.message`.
|
||||
* `state_key`: Setting this will result in a state event being sent.
|
||||
|
||||
|
||||
Once the notice has been sent, the APU will return the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "<event_id>"
|
||||
}
|
||||
```
|
||||
|
||||
Note that server notices must be enabled in `homeserver.yaml` before this API
|
||||
can be used. See [server_notices.md](../server_notices.md) for more information.
|
|
@ -5,7 +5,7 @@ This API returns information about a specific user account.
|
|||
|
||||
The api is::
|
||||
|
||||
GET /_matrix/client/r0/admin/whois/<user_id>
|
||||
GET /_synapse/admin/v1/whois/<user_id>
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
|
@ -50,7 +50,7 @@ references to it).
|
|||
|
||||
The api is::
|
||||
|
||||
POST /_matrix/client/r0/admin/deactivate/<user_id>
|
||||
POST /_synapse/admin/v1/deactivate/<user_id>
|
||||
|
||||
with a body of:
|
||||
|
||||
|
@ -73,7 +73,7 @@ Changes the password of another user.
|
|||
|
||||
The api is::
|
||||
|
||||
POST /_matrix/client/r0/admin/reset_password/<user_id>
|
||||
POST /_synapse/admin/v1/reset_password/<user_id>
|
||||
|
||||
with a body of:
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ contains Synapse version information).
|
|||
|
||||
The api is::
|
||||
|
||||
GET /_matrix/client/r0/admin/server_version
|
||||
GET /_synapse/admin/v1/server_version
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
|
|
|
@ -136,8 +136,8 @@ pid_file: DATADIR/homeserver.pid
|
|||
#
|
||||
# Valid resource names are:
|
||||
#
|
||||
# client: the client-server API (/_matrix/client). Also implies 'media' and
|
||||
# 'static'.
|
||||
# client: the client-server API (/_matrix/client), and the synapse admin
|
||||
# API (/_synapse/admin). Also implies 'media' and 'static'.
|
||||
#
|
||||
# consent: user consent forms (/_matrix/consent). See
|
||||
# docs/consent_tracking.md.
|
||||
|
@ -236,6 +236,14 @@ listeners:
|
|||
# - medium: 'email'
|
||||
# address: 'reserved_user@example.com'
|
||||
|
||||
# Used by phonehome stats to group together related servers.
|
||||
#server_context: context
|
||||
|
||||
# Whether to require a user to be in the room to add an alias to it.
|
||||
# Defaults to 'true'.
|
||||
#
|
||||
#require_membership_for_aliases: false
|
||||
|
||||
|
||||
## TLS ##
|
||||
|
||||
|
@ -257,6 +265,40 @@ listeners:
|
|||
#
|
||||
#tls_private_key_path: "CONFDIR/SERVERNAME.tls.key"
|
||||
|
||||
# Whether to verify TLS certificates when sending federation traffic.
|
||||
#
|
||||
# This currently defaults to `false`, however this will change in
|
||||
# Synapse 1.0 when valid federation certificates will be required.
|
||||
#
|
||||
#federation_verify_certificates: true
|
||||
|
||||
# Skip federation certificate verification on the following whitelist
|
||||
# of domains.
|
||||
#
|
||||
# This setting should only be used in very specific cases, such as
|
||||
# federation over Tor hidden services and similar. For private networks
|
||||
# of homeservers, you likely want to use a private CA instead.
|
||||
#
|
||||
# Only effective if federation_verify_certicates is `true`.
|
||||
#
|
||||
#federation_certificate_verification_whitelist:
|
||||
# - lon.example.com
|
||||
# - *.domain.com
|
||||
# - *.onion
|
||||
|
||||
# List of custom certificate authorities for federation traffic.
|
||||
#
|
||||
# This setting should only normally be used within a private network of
|
||||
# homeservers.
|
||||
#
|
||||
# Note that this list will replace those that are provided by your
|
||||
# operating environment. Certificates must be in PEM format.
|
||||
#
|
||||
#federation_custom_ca_list:
|
||||
# - myCA1.pem
|
||||
# - myCA2.pem
|
||||
# - myCA3.pem
|
||||
|
||||
# ACME support: This will configure Synapse to request a valid TLS certificate
|
||||
# for your configured `server_name` via Let's Encrypt.
|
||||
#
|
||||
|
@ -643,6 +685,32 @@ uploads_path: "DATADIR/uploads"
|
|||
#
|
||||
#enable_registration: false
|
||||
|
||||
# Optional account validity configuration. This allows for accounts to be denied
|
||||
# any request after a given period.
|
||||
#
|
||||
# ``enabled`` defines whether the account validity feature is enabled. Defaults
|
||||
# to False.
|
||||
#
|
||||
# ``period`` allows setting the period after which an account is valid
|
||||
# after its registration. When renewing the account, its validity period
|
||||
# will be extended by this amount of time. This parameter is required when using
|
||||
# the account validity feature.
|
||||
#
|
||||
# ``renew_at`` is the amount of time before an account's expiry date at which
|
||||
# Synapse will send an email to the account's email address with a renewal link.
|
||||
# This needs the ``email`` and ``public_baseurl`` configuration sections to be
|
||||
# filled.
|
||||
#
|
||||
# ``renew_email_subject`` is the subject of the email sent out with the renewal
|
||||
# link. ``%(app)s`` can be used as a placeholder for the ``app_name`` parameter
|
||||
# from the ``email`` section.
|
||||
#
|
||||
#account_validity:
|
||||
# enabled: True
|
||||
# period: 6w
|
||||
# renew_at: 1w
|
||||
# renew_email_subject: "Renew your %(app)s account"
|
||||
|
||||
# The user must provide all of the below types of 3PID when registering.
|
||||
#
|
||||
#registrations_require_3pid:
|
||||
|
@ -665,6 +733,10 @@ uploads_path: "DATADIR/uploads"
|
|||
# - medium: msisdn
|
||||
# pattern: '\+44'
|
||||
|
||||
# Enable 3PIDs lookup requests to identity servers from this server.
|
||||
#
|
||||
#enable_3pid_lookup: true
|
||||
|
||||
# If set, allows registration of standard or admin accounts by anyone who
|
||||
# has the shared secret, even if registration is otherwise disabled.
|
||||
#
|
||||
|
@ -884,7 +956,7 @@ password_config:
|
|||
|
||||
|
||||
|
||||
# Enable sending emails for notification events
|
||||
# Enable sending emails for notification events or expiry notices
|
||||
# Defining a custom URL for Riot is only needed if email notifications
|
||||
# should contain links to a self-hosted installation of Riot; when set
|
||||
# the "app_name" setting is ignored.
|
||||
|
@ -906,6 +978,9 @@ password_config:
|
|||
# #template_dir: res/templates
|
||||
# notif_template_html: notif_mail.html
|
||||
# notif_template_text: notif_mail.txt
|
||||
# # Templates for account expiry notices.
|
||||
# expiry_template_html: notice_expiry.html
|
||||
# expiry_template_text: notice_expiry.txt
|
||||
# notif_for_new_users: True
|
||||
# riot_base_url: "http://localhost/riot"
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
Server Notices
|
||||
==============
|
||||
# Server Notices
|
||||
|
||||
'Server Notices' are a new feature introduced in Synapse 0.30. They provide a
|
||||
channel whereby server administrators can send messages to users on the server.
|
||||
|
@ -11,8 +10,7 @@ they may also find a use for features such as "Message of the day".
|
|||
This is a feature specific to Synapse, but it uses standard Matrix
|
||||
communication mechanisms, so should work with any Matrix client.
|
||||
|
||||
User experience
|
||||
---------------
|
||||
## User experience
|
||||
|
||||
When the user is first sent a server notice, they will get an invitation to a
|
||||
room (typically called 'Server Notices', though this is configurable in
|
||||
|
@ -29,8 +27,7 @@ levels.
|
|||
Having joined the room, the user can leave the room if they want. Subsequent
|
||||
server notices will then cause a new room to be created.
|
||||
|
||||
Synapse configuration
|
||||
---------------------
|
||||
## Synapse configuration
|
||||
|
||||
Server notices come from a specific user id on the server. Server
|
||||
administrators are free to choose the user id - something like `server` is
|
||||
|
@ -58,17 +55,7 @@ room which will be created.
|
|||
`system_mxid_display_name` and `system_mxid_avatar_url` can be used to set the
|
||||
displayname and avatar of the Server Notices user.
|
||||
|
||||
Sending notices
|
||||
---------------
|
||||
## Sending notices
|
||||
|
||||
As of the current version of synapse, there is no convenient interface for
|
||||
sending notices (other than the automated ones sent as part of consent
|
||||
tracking).
|
||||
|
||||
In the meantime, it is possible to test this feature using the manhole. Having
|
||||
gone into the manhole as described in [manhole.md](manhole.md), a notice can be
|
||||
sent with something like:
|
||||
|
||||
```
|
||||
>>> hs.get_server_notices_manager().send_notice('@user:server.com', {'msgtype':'m.text', 'body':'foo'})
|
||||
```
|
||||
To send server notices to users you can use the
|
||||
[admin_api](admin_api/server_notices.md).
|
||||
|
|
|
@ -58,15 +58,11 @@ BOOLEAN_COLUMNS = {
|
|||
|
||||
|
||||
APPEND_ONLY_TABLES = [
|
||||
"event_content_hashes",
|
||||
"event_reference_hashes",
|
||||
"event_signatures",
|
||||
"event_edge_hashes",
|
||||
"events",
|
||||
"event_json",
|
||||
"state_events",
|
||||
"room_memberships",
|
||||
"feedback",
|
||||
"topics",
|
||||
"room_names",
|
||||
"rooms",
|
||||
|
@ -88,7 +84,6 @@ APPEND_ONLY_TABLES = [
|
|||
"event_search",
|
||||
"presence_stream",
|
||||
"push_rules_stream",
|
||||
"current_state_resets",
|
||||
"ex_outlier_stream",
|
||||
"cache_invalidation_stream",
|
||||
"public_room_list_stream",
|
||||
|
|
6
setup.py
6
setup.py
|
@ -86,13 +86,9 @@ long_description = read_file(("README.rst",))
|
|||
|
||||
REQUIREMENTS = dependencies['REQUIREMENTS']
|
||||
CONDITIONAL_REQUIREMENTS = dependencies['CONDITIONAL_REQUIREMENTS']
|
||||
ALL_OPTIONAL_REQUIREMENTS = dependencies['ALL_OPTIONAL_REQUIREMENTS']
|
||||
|
||||
# Make `pip install matrix-synapse[all]` install all the optional dependencies.
|
||||
ALL_OPTIONAL_REQUIREMENTS = set()
|
||||
|
||||
for optional_deps in CONDITIONAL_REQUIREMENTS.values():
|
||||
ALL_OPTIONAL_REQUIREMENTS = set(optional_deps) | ALL_OPTIONAL_REQUIREMENTS
|
||||
|
||||
CONDITIONAL_REQUIREMENTS["all"] = list(ALL_OPTIONAL_REQUIREMENTS)
|
||||
|
||||
|
||||
|
|
|
@ -64,6 +64,8 @@ class Auth(object):
|
|||
self.token_cache = LruCache(CACHE_SIZE_FACTOR * 10000)
|
||||
register_cache("cache", "token_cache", self.token_cache)
|
||||
|
||||
self._account_validity = hs.config.account_validity
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_from_context(self, room_version, event, context, do_sig_check=True):
|
||||
prev_state_ids = yield context.get_prev_state_ids(self.store)
|
||||
|
@ -226,6 +228,17 @@ class Auth(object):
|
|||
token_id = user_info["token_id"]
|
||||
is_guest = user_info["is_guest"]
|
||||
|
||||
# Deny the request if the user account has expired.
|
||||
if self._account_validity.enabled:
|
||||
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:
|
||||
raise AuthError(
|
||||
403,
|
||||
"User account has expired",
|
||||
errcode=Codes.EXPIRED_ACCOUNT,
|
||||
)
|
||||
|
||||
# device_id may not be present if get_user_by_access_token has been
|
||||
# stubbed out.
|
||||
device_id = user_info.get("device_id")
|
||||
|
@ -543,7 +556,7 @@ class Auth(object):
|
|||
""" Check if the given user is a local server admin.
|
||||
|
||||
Args:
|
||||
user (str): mxid of user to check
|
||||
user (UserID): user to check
|
||||
|
||||
Returns:
|
||||
bool: True if the user is an admin
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2018 New Vector 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.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector 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.
|
||||
|
@ -60,6 +60,7 @@ class Codes(object):
|
|||
UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
|
||||
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
|
||||
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
|
||||
|
||||
|
||||
class CodeMessageException(RuntimeError):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector 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.
|
||||
|
|
|
@ -62,6 +62,7 @@ from synapse.python_dependencies import check_requirements
|
|||
from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
|
||||
from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory
|
||||
from synapse.rest import ClientRestResource
|
||||
from synapse.rest.admin import AdminRestResource
|
||||
from synapse.rest.key.v2 import KeyApiV2Resource
|
||||
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
||||
from synapse.rest.well_known import WellKnownResource
|
||||
|
@ -180,6 +181,7 @@ class SynapseHomeServer(HomeServer):
|
|||
"/_matrix/client/v2_alpha": client_resource,
|
||||
"/_matrix/client/versions": client_resource,
|
||||
"/.well-known/matrix/client": WellKnownResource(self),
|
||||
"/_synapse/admin": AdminRestResource(self),
|
||||
})
|
||||
|
||||
if self.get_config().saml2_enabled:
|
||||
|
@ -518,6 +520,7 @@ def run(hs):
|
|||
uptime = 0
|
||||
|
||||
stats["homeserver"] = hs.config.server_name
|
||||
stats["server_context"] = hs.config.server_context
|
||||
stats["timestamp"] = now
|
||||
stats["uptime_seconds"] = uptime
|
||||
version = sys.version_info
|
||||
|
@ -558,7 +561,6 @@ def run(hs):
|
|||
|
||||
stats["database_engine"] = hs.get_datastore().database_engine_name
|
||||
stats["database_server_version"] = hs.get_datastore().get_server_version()
|
||||
|
||||
logger.info("Reporting stats to matrix.org: %s" % (stats,))
|
||||
try:
|
||||
yield hs.get_simple_http_client().put_json(
|
||||
|
|
|
@ -71,6 +71,12 @@ class EmailConfig(Config):
|
|||
self.email_notif_from = email_config["notif_from"]
|
||||
self.email_notif_template_html = email_config["notif_template_html"]
|
||||
self.email_notif_template_text = email_config["notif_template_text"]
|
||||
self.email_expiry_template_html = email_config.get(
|
||||
"expiry_template_html", "notice_expiry.html",
|
||||
)
|
||||
self.email_expiry_template_text = email_config.get(
|
||||
"expiry_template_text", "notice_expiry.txt",
|
||||
)
|
||||
|
||||
template_dir = email_config.get("template_dir")
|
||||
# we need an absolute path, because we change directory after starting (and
|
||||
|
@ -120,7 +126,7 @@ class EmailConfig(Config):
|
|||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """
|
||||
# Enable sending emails for notification events
|
||||
# Enable sending emails for notification events or expiry notices
|
||||
# Defining a custom URL for Riot is only needed if email notifications
|
||||
# should contain links to a self-hosted installation of Riot; when set
|
||||
# the "app_name" setting is ignored.
|
||||
|
@ -142,6 +148,9 @@ class EmailConfig(Config):
|
|||
# #template_dir: res/templates
|
||||
# notif_template_html: notif_mail.html
|
||||
# notif_template_text: notif_mail.txt
|
||||
# # Templates for account expiry notices.
|
||||
# expiry_template_html: notice_expiry.html
|
||||
# expiry_template_text: notice_expiry.txt
|
||||
# notif_for_new_users: True
|
||||
# riot_base_url: "http://localhost/riot"
|
||||
"""
|
||||
|
|
|
@ -20,6 +20,29 @@ from synapse.types import RoomAlias
|
|||
from synapse.util.stringutils import random_string_with_symbols
|
||||
|
||||
|
||||
class AccountValidityConfig(Config):
|
||||
def __init__(self, config, synapse_config):
|
||||
self.enabled = config.get("enabled", False)
|
||||
self.renew_by_email_enabled = ("renew_at" in config)
|
||||
|
||||
if self.enabled:
|
||||
if "period" in config:
|
||||
self.period = self.parse_duration(config["period"])
|
||||
else:
|
||||
raise ConfigError("'period' is required when using account validity")
|
||||
|
||||
if "renew_at" in config:
|
||||
self.renew_at = self.parse_duration(config["renew_at"])
|
||||
|
||||
if "renew_email_subject" in config:
|
||||
self.renew_email_subject = config["renew_email_subject"]
|
||||
else:
|
||||
self.renew_email_subject = "Renew your %(app)s account"
|
||||
|
||||
if self.renew_by_email_enabled and "public_baseurl" not in synapse_config:
|
||||
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
|
||||
|
||||
|
||||
class RegistrationConfig(Config):
|
||||
|
||||
def read_config(self, config):
|
||||
|
@ -31,8 +54,13 @@ class RegistrationConfig(Config):
|
|||
strtobool(str(config["disable_registration"]))
|
||||
)
|
||||
|
||||
self.account_validity = AccountValidityConfig(
|
||||
config.get("account_validity", {}), config,
|
||||
)
|
||||
|
||||
self.registrations_require_3pid = config.get("registrations_require_3pid", [])
|
||||
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
|
||||
self.enable_3pid_lookup = config.get("enable_3pid_lookup", True)
|
||||
self.registration_shared_secret = config.get("registration_shared_secret")
|
||||
|
||||
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
|
||||
|
@ -75,6 +103,32 @@ class RegistrationConfig(Config):
|
|||
#
|
||||
#enable_registration: false
|
||||
|
||||
# Optional account validity configuration. This allows for accounts to be denied
|
||||
# any request after a given period.
|
||||
#
|
||||
# ``enabled`` defines whether the account validity feature is enabled. Defaults
|
||||
# to False.
|
||||
#
|
||||
# ``period`` allows setting the period after which an account is valid
|
||||
# after its registration. When renewing the account, its validity period
|
||||
# will be extended by this amount of time. This parameter is required when using
|
||||
# the account validity feature.
|
||||
#
|
||||
# ``renew_at`` is the amount of time before an account's expiry date at which
|
||||
# Synapse will send an email to the account's email address with a renewal link.
|
||||
# This needs the ``email`` and ``public_baseurl`` configuration sections to be
|
||||
# filled.
|
||||
#
|
||||
# ``renew_email_subject`` is the subject of the email sent out with the renewal
|
||||
# link. ``%%(app)s`` can be used as a placeholder for the ``app_name`` parameter
|
||||
# from the ``email`` section.
|
||||
#
|
||||
#account_validity:
|
||||
# enabled: True
|
||||
# period: 6w
|
||||
# renew_at: 1w
|
||||
# renew_email_subject: "Renew your %%(app)s account"
|
||||
|
||||
# The user must provide all of the below types of 3PID when registering.
|
||||
#
|
||||
#registrations_require_3pid:
|
||||
|
@ -97,6 +151,10 @@ class RegistrationConfig(Config):
|
|||
# - medium: msisdn
|
||||
# pattern: '\\+44'
|
||||
|
||||
# Enable 3PIDs lookup requests to identity servers from this server.
|
||||
#
|
||||
#enable_3pid_lookup: true
|
||||
|
||||
# If set, allows registration of standard or admin accounts by anyone who
|
||||
# has the shared secret, even if registration is otherwise disabled.
|
||||
#
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector 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.
|
||||
|
|
|
@ -37,6 +37,7 @@ class ServerConfig(Config):
|
|||
|
||||
def read_config(self, config):
|
||||
self.server_name = config["server_name"]
|
||||
self.server_context = config.get("server_context", None)
|
||||
|
||||
try:
|
||||
parse_and_validate_server_name(self.server_name)
|
||||
|
@ -113,11 +114,13 @@ class ServerConfig(Config):
|
|||
# FIXME: federation_domain_whitelist needs sytests
|
||||
self.federation_domain_whitelist = None
|
||||
federation_domain_whitelist = config.get(
|
||||
"federation_domain_whitelist", None
|
||||
"federation_domain_whitelist", None,
|
||||
)
|
||||
# turn the whitelist into a hash for speed of lookup
|
||||
|
||||
if federation_domain_whitelist is not None:
|
||||
# turn the whitelist into a hash for speed of lookup
|
||||
self.federation_domain_whitelist = {}
|
||||
|
||||
for domain in federation_domain_whitelist:
|
||||
self.federation_domain_whitelist[domain] = True
|
||||
|
||||
|
@ -131,6 +134,12 @@ class ServerConfig(Config):
|
|||
# sending out any replication updates.
|
||||
self.replication_torture_level = config.get("replication_torture_level")
|
||||
|
||||
# Whether to require a user to be in the room to add an alias to it.
|
||||
# Defaults to True.
|
||||
self.require_membership_for_aliases = config.get(
|
||||
"require_membership_for_aliases", True,
|
||||
)
|
||||
|
||||
self.listeners = []
|
||||
for listener in config.get("listeners", []):
|
||||
if not isinstance(listener.get("port", None), int):
|
||||
|
@ -385,8 +394,8 @@ class ServerConfig(Config):
|
|||
#
|
||||
# Valid resource names are:
|
||||
#
|
||||
# client: the client-server API (/_matrix/client). Also implies 'media' and
|
||||
# 'static'.
|
||||
# client: the client-server API (/_matrix/client), and the synapse admin
|
||||
# API (/_synapse/admin). Also implies 'media' and 'static'.
|
||||
#
|
||||
# consent: user consent forms (/_matrix/consent). See
|
||||
# docs/consent_tracking.md.
|
||||
|
@ -484,6 +493,14 @@ class ServerConfig(Config):
|
|||
#mau_limit_reserved_threepids:
|
||||
# - medium: 'email'
|
||||
# address: 'reserved_user@example.com'
|
||||
|
||||
# Used by phonehome stats to group together related servers.
|
||||
#server_context: context
|
||||
|
||||
# Whether to require a user to be in the room to add an alias to it.
|
||||
# Defaults to 'true'.
|
||||
#
|
||||
#require_membership_for_aliases: false
|
||||
""" % locals()
|
||||
|
||||
def read_arguments(self, args):
|
||||
|
|
|
@ -24,8 +24,10 @@ import six
|
|||
from unpaddedbase64 import encode_base64
|
||||
|
||||
from OpenSSL import crypto
|
||||
from twisted.internet._sslverify import Certificate, trustRootFromCertificates
|
||||
|
||||
from synapse.config._base import Config, ConfigError
|
||||
from synapse.util import glob_to_regex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -70,6 +72,53 @@ class TlsConfig(Config):
|
|||
|
||||
self.tls_fingerprints = list(self._original_tls_fingerprints)
|
||||
|
||||
# Whether to verify certificates on outbound federation traffic
|
||||
self.federation_verify_certificates = config.get(
|
||||
"federation_verify_certificates", False,
|
||||
)
|
||||
|
||||
# Whitelist of domains to not verify certificates for
|
||||
fed_whitelist_entries = config.get(
|
||||
"federation_certificate_verification_whitelist", [],
|
||||
)
|
||||
|
||||
# Support globs (*) in whitelist values
|
||||
self.federation_certificate_verification_whitelist = []
|
||||
for entry in fed_whitelist_entries:
|
||||
# Convert globs to regex
|
||||
entry_regex = glob_to_regex(entry)
|
||||
self.federation_certificate_verification_whitelist.append(entry_regex)
|
||||
|
||||
# List of custom certificate authorities for federation traffic validation
|
||||
custom_ca_list = config.get(
|
||||
"federation_custom_ca_list", None,
|
||||
)
|
||||
|
||||
# Read in and parse custom CA certificates
|
||||
self.federation_ca_trust_root = None
|
||||
if custom_ca_list is not None:
|
||||
if len(custom_ca_list) == 0:
|
||||
# A trustroot cannot be generated without any CA certificates.
|
||||
# Raise an error if this option has been specified without any
|
||||
# corresponding certificates.
|
||||
raise ConfigError("federation_custom_ca_list specified without "
|
||||
"any certificate files")
|
||||
|
||||
certs = []
|
||||
for ca_file in custom_ca_list:
|
||||
logger.debug("Reading custom CA certificate file: %s", ca_file)
|
||||
content = self.read_file(ca_file)
|
||||
|
||||
# Parse the CA certificates
|
||||
try:
|
||||
cert_base = Certificate.loadPEM(content)
|
||||
certs.append(cert_base)
|
||||
except Exception as e:
|
||||
raise ConfigError("Error parsing custom CA certificate file %s: %s"
|
||||
% (ca_file, e))
|
||||
|
||||
self.federation_ca_trust_root = trustRootFromCertificates(certs)
|
||||
|
||||
# This config option applies to non-federation HTTP clients
|
||||
# (e.g. for talking to recaptcha, identity servers, and such)
|
||||
# It should never be used in production, and is intended for
|
||||
|
@ -99,15 +148,15 @@ class TlsConfig(Config):
|
|||
try:
|
||||
with open(self.tls_certificate_file, 'rb') as f:
|
||||
cert_pem = f.read()
|
||||
except Exception:
|
||||
logger.exception("Failed to read existing certificate off disk!")
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ConfigError("Failed to read existing certificate file %s: %s"
|
||||
% (self.tls_certificate_file, e))
|
||||
|
||||
try:
|
||||
tls_certificate = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
||||
except Exception:
|
||||
logger.exception("Failed to parse existing certificate off disk!")
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ConfigError("Failed to parse existing certificate file %s: %s"
|
||||
% (self.tls_certificate_file, e))
|
||||
|
||||
if not allow_self_signed:
|
||||
if tls_certificate.get_subject() == tls_certificate.get_issuer():
|
||||
|
@ -192,6 +241,40 @@ class TlsConfig(Config):
|
|||
#
|
||||
#tls_private_key_path: "%(tls_private_key_path)s"
|
||||
|
||||
# Whether to verify TLS certificates when sending federation traffic.
|
||||
#
|
||||
# This currently defaults to `false`, however this will change in
|
||||
# Synapse 1.0 when valid federation certificates will be required.
|
||||
#
|
||||
#federation_verify_certificates: true
|
||||
|
||||
# Skip federation certificate verification on the following whitelist
|
||||
# of domains.
|
||||
#
|
||||
# This setting should only be used in very specific cases, such as
|
||||
# federation over Tor hidden services and similar. For private networks
|
||||
# of homeservers, you likely want to use a private CA instead.
|
||||
#
|
||||
# Only effective if federation_verify_certicates is `true`.
|
||||
#
|
||||
#federation_certificate_verification_whitelist:
|
||||
# - lon.example.com
|
||||
# - *.domain.com
|
||||
# - *.onion
|
||||
|
||||
# List of custom certificate authorities for federation traffic.
|
||||
#
|
||||
# This setting should only normally be used within a private network of
|
||||
# homeservers.
|
||||
#
|
||||
# Note that this list will replace those that are provided by your
|
||||
# operating environment. Certificates must be in PEM format.
|
||||
#
|
||||
#federation_custom_ca_list:
|
||||
# - myCA1.pem
|
||||
# - myCA2.pem
|
||||
# - myCA3.pem
|
||||
|
||||
# ACME support: This will configure Synapse to request a valid TLS certificate
|
||||
# for your configured `server_name` via Let's Encrypt.
|
||||
#
|
||||
|
|
|
@ -18,10 +18,10 @@ import logging
|
|||
from zope.interface import implementer
|
||||
|
||||
from OpenSSL import SSL, crypto
|
||||
from twisted.internet._sslverify import _defaultCurveName
|
||||
from twisted.internet._sslverify import ClientTLSOptions, _defaultCurveName
|
||||
from twisted.internet.abstract import isIPAddress, isIPv6Address
|
||||
from twisted.internet.interfaces import IOpenSSLClientConnectionCreator
|
||||
from twisted.internet.ssl import CertificateOptions, ContextFactory
|
||||
from twisted.internet.ssl import CertificateOptions, ContextFactory, platformTrust
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -90,7 +90,7 @@ def _tolerateErrors(wrapped):
|
|||
|
||||
|
||||
@implementer(IOpenSSLClientConnectionCreator)
|
||||
class ClientTLSOptions(object):
|
||||
class ClientTLSOptionsNoVerify(object):
|
||||
"""
|
||||
Client creator for TLS without certificate identity verification. This is a
|
||||
copy of twisted.internet._sslverify.ClientTLSOptions with the identity
|
||||
|
@ -127,9 +127,30 @@ class ClientTLSOptionsFactory(object):
|
|||
to remote servers for federation."""
|
||||
|
||||
def __init__(self, config):
|
||||
# We don't use config options yet
|
||||
self._options = CertificateOptions(verify=False)
|
||||
self._config = config
|
||||
self._options_noverify = CertificateOptions()
|
||||
|
||||
# Check if we're using a custom list of a CA certificates
|
||||
trust_root = config.federation_ca_trust_root
|
||||
if trust_root is None:
|
||||
# Use CA root certs provided by OpenSSL
|
||||
trust_root = platformTrust()
|
||||
|
||||
self._options_verify = CertificateOptions(trustRoot=trust_root)
|
||||
|
||||
def get_options(self, host):
|
||||
# Use _makeContext so that we get a fresh OpenSSL CTX each time.
|
||||
return ClientTLSOptions(host, self._options._makeContext())
|
||||
|
||||
# Check if certificate verification has been enabled
|
||||
should_verify = self._config.federation_verify_certificates
|
||||
|
||||
# Check if we've disabled certificate verification for this host
|
||||
if should_verify:
|
||||
for regex in self._config.federation_certificate_verification_whitelist:
|
||||
if regex.match(host):
|
||||
should_verify = False
|
||||
break
|
||||
|
||||
if should_verify:
|
||||
return ClientTLSOptions(host, self._options_verify._makeContext())
|
||||
return ClientTLSOptionsNoVerify(host, self._options_noverify._makeContext())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017, 2018 New Vector Ltd.
|
||||
# Copyright 2017, 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.
|
||||
|
@ -20,6 +20,7 @@ from collections import namedtuple
|
|||
from six import raise_from
|
||||
from six.moves import urllib
|
||||
|
||||
import nacl.signing
|
||||
from signedjson.key import (
|
||||
decode_verify_key_bytes,
|
||||
encode_verify_key_base64,
|
||||
|
@ -113,40 +114,54 @@ class Keyring(object):
|
|||
server_name. The deferreds run their callbacks in the sentinel
|
||||
logcontext.
|
||||
"""
|
||||
# a list of VerifyKeyRequests
|
||||
verify_requests = []
|
||||
handle = preserve_fn(_handle_key_deferred)
|
||||
|
||||
for server_name, json_object in server_and_json:
|
||||
def process(server_name, json_object):
|
||||
"""Process an entry in the request list
|
||||
|
||||
Given a (server_name, json_object) pair from the request list,
|
||||
adds a key request to verify_requests, and returns a deferred which will
|
||||
complete or fail (in the sentinel context) when verification completes.
|
||||
"""
|
||||
key_ids = signature_ids(json_object, server_name)
|
||||
|
||||
if not key_ids:
|
||||
logger.warn("Request from %s: no supported signature keys",
|
||||
server_name)
|
||||
deferred = defer.fail(SynapseError(
|
||||
400,
|
||||
"Not signed with a supported algorithm",
|
||||
Codes.UNAUTHORIZED,
|
||||
))
|
||||
else:
|
||||
deferred = defer.Deferred()
|
||||
return defer.fail(
|
||||
SynapseError(
|
||||
400,
|
||||
"Not signed by %s" % (server_name,),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug("Verifying for %s with key_ids %s",
|
||||
server_name, key_ids)
|
||||
|
||||
# add the key request to the queue, but don't start it off yet.
|
||||
verify_request = VerifyKeyRequest(
|
||||
server_name, key_ids, json_object, deferred
|
||||
server_name, key_ids, json_object, defer.Deferred(),
|
||||
)
|
||||
|
||||
verify_requests.append(verify_request)
|
||||
|
||||
run_in_background(self._start_key_lookups, verify_requests)
|
||||
# now run _handle_key_deferred, which will wait for the key request
|
||||
# to complete and then do the verification.
|
||||
#
|
||||
# We want _handle_key_request to log to the right context, so we
|
||||
# wrap it with preserve_fn (aka run_in_background)
|
||||
return handle(verify_request)
|
||||
|
||||
# Pass those keys to handle_key_deferred so that the json object
|
||||
# signatures can be verified
|
||||
handle = preserve_fn(_handle_key_deferred)
|
||||
return [
|
||||
handle(rq) for rq in verify_requests
|
||||
results = [
|
||||
process(server_name, json_object)
|
||||
for server_name, json_object in server_and_json
|
||||
]
|
||||
|
||||
if verify_requests:
|
||||
run_in_background(self._start_key_lookups, verify_requests)
|
||||
|
||||
return results
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _start_key_lookups(self, verify_requests):
|
||||
"""Sets off the key fetches for each verify request
|
||||
|
@ -274,10 +289,6 @@ class Keyring(object):
|
|||
@defer.inlineCallbacks
|
||||
def do_iterations():
|
||||
with Measure(self.clock, "get_server_verify_keys"):
|
||||
# dict[str, dict[str, VerifyKey]]: results so far.
|
||||
# map server_name -> key_id -> VerifyKey
|
||||
merged_results = {}
|
||||
|
||||
# dict[str, set(str)]: keys to fetch for each server
|
||||
missing_keys = {}
|
||||
for verify_request in verify_requests:
|
||||
|
@ -287,29 +298,29 @@ class Keyring(object):
|
|||
|
||||
for fn in key_fetch_fns:
|
||||
results = yield fn(missing_keys.items())
|
||||
merged_results.update(results)
|
||||
|
||||
# We now need to figure out which verify requests we have keys
|
||||
# for and which we don't
|
||||
missing_keys = {}
|
||||
requests_missing_keys = []
|
||||
for verify_request in verify_requests:
|
||||
server_name = verify_request.server_name
|
||||
result_keys = merged_results[server_name]
|
||||
|
||||
if verify_request.deferred.called:
|
||||
# We've already called this deferred, which probably
|
||||
# means that we've already found a key for it.
|
||||
continue
|
||||
|
||||
server_name = verify_request.server_name
|
||||
|
||||
# see if any of the keys we got this time are sufficient to
|
||||
# complete this VerifyKeyRequest.
|
||||
result_keys = results.get(server_name, {})
|
||||
for key_id in verify_request.key_ids:
|
||||
if key_id in result_keys:
|
||||
key = result_keys.get(key_id)
|
||||
if key:
|
||||
with PreserveLoggingContext():
|
||||
verify_request.deferred.callback((
|
||||
server_name,
|
||||
key_id,
|
||||
result_keys[key_id],
|
||||
))
|
||||
verify_request.deferred.callback(
|
||||
(server_name, key_id, key)
|
||||
)
|
||||
break
|
||||
else:
|
||||
# The else block is only reached if the loop above
|
||||
|
@ -343,27 +354,24 @@ class Keyring(object):
|
|||
@defer.inlineCallbacks
|
||||
def get_keys_from_store(self, server_name_and_key_ids):
|
||||
"""
|
||||
|
||||
Args:
|
||||
server_name_and_key_ids (list[(str, iterable[str])]):
|
||||
server_name_and_key_ids (iterable(Tuple[str, iterable[str]]):
|
||||
list of (server_name, iterable[key_id]) tuples to fetch keys for
|
||||
|
||||
Returns:
|
||||
Deferred: resolves to dict[str, dict[str, VerifyKey]]: map from
|
||||
Deferred: resolves to dict[str, dict[str, VerifyKey|None]]: map from
|
||||
server_name -> key_id -> VerifyKey
|
||||
"""
|
||||
res = yield logcontext.make_deferred_yieldable(defer.gatherResults(
|
||||
[
|
||||
run_in_background(
|
||||
self.store.get_server_verify_keys,
|
||||
server_name, key_ids,
|
||||
).addCallback(lambda ks, server: (server, ks), server_name)
|
||||
for server_name, key_ids in server_name_and_key_ids
|
||||
],
|
||||
consumeErrors=True,
|
||||
).addErrback(unwrapFirstError))
|
||||
|
||||
defer.returnValue(dict(res))
|
||||
keys_to_fetch = (
|
||||
(server_name, key_id)
|
||||
for server_name, key_ids in server_name_and_key_ids
|
||||
for key_id in key_ids
|
||||
)
|
||||
res = yield self.store.get_server_verify_keys(keys_to_fetch)
|
||||
keys = {}
|
||||
for (server_name, key_id), key in res.items():
|
||||
keys.setdefault(server_name, {})[key_id] = key
|
||||
defer.returnValue(keys)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_keys_from_perspectives(self, server_name_and_key_ids):
|
||||
|
@ -494,11 +502,11 @@ class Keyring(object):
|
|||
)
|
||||
|
||||
processed_response = yield self.process_v2_response(
|
||||
perspective_name, response, only_from_server=False
|
||||
perspective_name, response
|
||||
)
|
||||
server_name = response["server_name"]
|
||||
|
||||
for server_name, response_keys in processed_response.items():
|
||||
keys.setdefault(server_name, {}).update(response_keys)
|
||||
keys.setdefault(server_name, {}).update(processed_response)
|
||||
|
||||
yield logcontext.make_deferred_yieldable(defer.gatherResults(
|
||||
[
|
||||
|
@ -517,7 +525,7 @@ class Keyring(object):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def get_server_verify_key_v2_direct(self, server_name, key_ids):
|
||||
keys = {}
|
||||
keys = {} # type: dict[str, nacl.signing.VerifyKey]
|
||||
|
||||
for requested_key_id in key_ids:
|
||||
if requested_key_id in keys:
|
||||
|
@ -542,6 +550,11 @@ class Keyring(object):
|
|||
or server_name not in response[u"signatures"]):
|
||||
raise KeyLookupError("Key response not signed by remote server")
|
||||
|
||||
if response["server_name"] != server_name:
|
||||
raise KeyLookupError("Expected a response for server %r not %r" % (
|
||||
server_name, response["server_name"]
|
||||
))
|
||||
|
||||
response_keys = yield self.process_v2_response(
|
||||
from_server=server_name,
|
||||
requested_ids=[requested_key_id],
|
||||
|
@ -550,24 +563,45 @@ class Keyring(object):
|
|||
|
||||
keys.update(response_keys)
|
||||
|
||||
yield logcontext.make_deferred_yieldable(defer.gatherResults(
|
||||
[
|
||||
run_in_background(
|
||||
self.store_keys,
|
||||
server_name=key_server_name,
|
||||
from_server=server_name,
|
||||
verify_keys=verify_keys,
|
||||
)
|
||||
for key_server_name, verify_keys in keys.items()
|
||||
],
|
||||
consumeErrors=True
|
||||
).addErrback(unwrapFirstError))
|
||||
|
||||
defer.returnValue(keys)
|
||||
yield self.store_keys(
|
||||
server_name=server_name,
|
||||
from_server=server_name,
|
||||
verify_keys=keys,
|
||||
)
|
||||
defer.returnValue({server_name: keys})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def process_v2_response(self, from_server, response_json,
|
||||
requested_ids=[], only_from_server=True):
|
||||
def process_v2_response(
|
||||
self, from_server, response_json, requested_ids=[],
|
||||
):
|
||||
"""Parse a 'Server Keys' structure from the result of a /key request
|
||||
|
||||
This is used to parse either the entirety of the response from
|
||||
GET /_matrix/key/v2/server, or a single entry from the list returned by
|
||||
POST /_matrix/key/v2/query.
|
||||
|
||||
Checks that each signature in the response that claims to come from the origin
|
||||
server is valid. (Does not check that there actually is such a signature, for
|
||||
some reason.)
|
||||
|
||||
Stores the json in server_keys_json so that it can be used for future responses
|
||||
to /_matrix/key/v2/query.
|
||||
|
||||
Args:
|
||||
from_server (str): the name of the server producing this result: either
|
||||
the origin server for a /_matrix/key/v2/server request, or the notary
|
||||
for a /_matrix/key/v2/query.
|
||||
|
||||
response_json (dict): the json-decoded Server Keys response object
|
||||
|
||||
requested_ids (iterable[str]): a list of the key IDs that were requested.
|
||||
We will store the json for these key ids as well as any that are
|
||||
actually in the response
|
||||
|
||||
Returns:
|
||||
Deferred[dict[str, nacl.signing.VerifyKey]]:
|
||||
map from key_id to key object
|
||||
"""
|
||||
time_now_ms = self.clock.time_msec()
|
||||
response_keys = {}
|
||||
verify_keys = {}
|
||||
|
@ -589,15 +623,7 @@ class Keyring(object):
|
|||
verify_key.time_added = time_now_ms
|
||||
old_verify_keys[key_id] = verify_key
|
||||
|
||||
results = {}
|
||||
server_name = response_json["server_name"]
|
||||
if only_from_server:
|
||||
if server_name != from_server:
|
||||
raise KeyLookupError(
|
||||
"Expected a response for server %r not %r" % (
|
||||
from_server, server_name
|
||||
)
|
||||
)
|
||||
for key_id in response_json["signatures"].get(server_name, {}):
|
||||
if key_id not in response_json["verify_keys"]:
|
||||
raise KeyLookupError(
|
||||
|
@ -633,7 +659,7 @@ class Keyring(object):
|
|||
self.store.store_server_keys_json,
|
||||
server_name=server_name,
|
||||
key_id=key_id,
|
||||
from_server=server_name,
|
||||
from_server=from_server,
|
||||
ts_now_ms=time_now_ms,
|
||||
ts_expires_ms=ts_valid_until_ms,
|
||||
key_json_bytes=signed_key_json_bytes,
|
||||
|
@ -643,9 +669,7 @@ class Keyring(object):
|
|||
consumeErrors=True,
|
||||
).addErrback(unwrapFirstError))
|
||||
|
||||
results[server_name] = response_keys
|
||||
|
||||
defer.returnValue(results)
|
||||
defer.returnValue(response_keys)
|
||||
|
||||
def store_keys(self, server_name, from_server, verify_keys):
|
||||
"""Store a collection of verify keys for a given server
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 New Vector Ltd.
|
||||
# Copyright 2017 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.
|
||||
|
|
|
@ -269,7 +269,18 @@ def _check_sigs_on_pdus(keyring, room_version, pdus):
|
|||
for p in pdus_to_check_sender
|
||||
])
|
||||
|
||||
def sender_err(e, pdu_to_check):
|
||||
errmsg = "event id %s: unable to verify signature for sender %s: %s" % (
|
||||
pdu_to_check.pdu.event_id,
|
||||
pdu_to_check.sender_domain,
|
||||
e.getErrorMessage(),
|
||||
)
|
||||
# XX not really sure if these are the right codes, but they are what
|
||||
# we've done for ages
|
||||
raise SynapseError(400, errmsg, Codes.UNAUTHORIZED)
|
||||
|
||||
for p, d in zip(pdus_to_check_sender, more_deferreds):
|
||||
d.addErrback(sender_err, p)
|
||||
p.deferreds.append(d)
|
||||
|
||||
# now let's look for events where the sender's domain is different to the
|
||||
|
@ -291,7 +302,18 @@ def _check_sigs_on_pdus(keyring, room_version, pdus):
|
|||
for p in pdus_to_check_event_id
|
||||
])
|
||||
|
||||
def event_err(e, pdu_to_check):
|
||||
errmsg = (
|
||||
"event id %s: unable to verify signature for event id domain: %s" % (
|
||||
pdu_to_check.pdu.event_id,
|
||||
e.getErrorMessage(),
|
||||
)
|
||||
)
|
||||
# XX as above: not really sure if these are the right codes
|
||||
raise SynapseError(400, errmsg, Codes.UNAUTHORIZED)
|
||||
|
||||
for p, d in zip(pdus_to_check_event_id, more_deferreds):
|
||||
d.addErrback(event_err, p)
|
||||
p.deferreds.append(d)
|
||||
|
||||
# replace lists of deferreds with single Deferreds
|
||||
|
|
253
synapse/handlers/account_validity.py
Normal file
253
synapse/handlers/account_validity.py
Normal file
|
@ -0,0 +1,253 @@
|
|||
# -*- 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.
|
||||
|
||||
import email.mime.multipart
|
||||
import email.utils
|
||||
import logging
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.types import UserID
|
||||
from synapse.util import stringutils
|
||||
from synapse.util.logcontext import make_deferred_yieldable
|
||||
|
||||
try:
|
||||
from synapse.push.mailer import load_jinja2_templates
|
||||
except ImportError:
|
||||
load_jinja2_templates = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountValidityHandler(object):
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
self.store = self.hs.get_datastore()
|
||||
self.sendmail = self.hs.get_sendmail()
|
||||
self.clock = self.hs.get_clock()
|
||||
|
||||
self._account_validity = self.hs.config.account_validity
|
||||
|
||||
if self._account_validity.renew_by_email_enabled and load_jinja2_templates:
|
||||
# Don't do email-specific configuration if renewal by email is disabled.
|
||||
try:
|
||||
app_name = self.hs.config.email_app_name
|
||||
|
||||
self._subject = self._account_validity.renew_email_subject % {
|
||||
"app": app_name,
|
||||
}
|
||||
|
||||
self._from_string = self.hs.config.email_notif_from % {
|
||||
"app": app_name,
|
||||
}
|
||||
except Exception:
|
||||
# If substitution failed, fall back to the bare strings.
|
||||
self._subject = self._account_validity.renew_email_subject
|
||||
self._from_string = self.hs.config.email_notif_from
|
||||
|
||||
self._raw_from = email.utils.parseaddr(self._from_string)[1]
|
||||
|
||||
self._template_html, self._template_text = load_jinja2_templates(
|
||||
config=self.hs.config,
|
||||
template_html_name=self.hs.config.email_expiry_template_html,
|
||||
template_text_name=self.hs.config.email_expiry_template_text,
|
||||
)
|
||||
|
||||
# Check the renewal emails to send and send them every 30min.
|
||||
self.clock.looping_call(
|
||||
self.send_renewal_emails,
|
||||
30 * 60 * 1000,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_renewal_emails(self):
|
||||
"""Gets the list of users whose account is expiring in the amount of time
|
||||
configured in the ``renew_at`` parameter from the ``account_validity``
|
||||
configuration, and sends renewal emails to all of these users as long as they
|
||||
have an email 3PID attached to their account.
|
||||
"""
|
||||
expiring_users = yield self.store.get_users_expiring_soon()
|
||||
|
||||
if expiring_users:
|
||||
for user in expiring_users:
|
||||
yield self._send_renewal_email(
|
||||
user_id=user["user_id"],
|
||||
expiration_ts=user["expiration_ts_ms"],
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_renewal_email_to_user(self, user_id):
|
||||
expiration_ts = yield self.store.get_expiration_ts_for_user(user_id)
|
||||
yield self._send_renewal_email(user_id, expiration_ts)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _send_renewal_email(self, user_id, expiration_ts):
|
||||
"""Sends out a renewal email to every email address attached to the given user
|
||||
with a unique link allowing them to renew their account.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to send email(s) to.
|
||||
expiration_ts (int): Timestamp in milliseconds for the expiration date of
|
||||
this user's account (used in the email templates).
|
||||
"""
|
||||
addresses = yield self._get_email_addresses_for_user(user_id)
|
||||
|
||||
# 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.
|
||||
if not addresses:
|
||||
return
|
||||
|
||||
try:
|
||||
user_display_name = yield self.store.get_profile_displayname(
|
||||
UserID.from_string(user_id).localpart
|
||||
)
|
||||
if user_display_name is None:
|
||||
user_display_name = user_id
|
||||
except StoreError:
|
||||
user_display_name = user_id
|
||||
|
||||
renewal_token = yield self._get_renewal_token(user_id)
|
||||
url = "%s_matrix/client/unstable/account_validity/renew?token=%s" % (
|
||||
self.hs.config.public_baseurl,
|
||||
renewal_token,
|
||||
)
|
||||
|
||||
template_vars = {
|
||||
"display_name": user_display_name,
|
||||
"expiration_ts": expiration_ts,
|
||||
"url": url,
|
||||
}
|
||||
|
||||
html_text = self._template_html.render(**template_vars)
|
||||
html_part = MIMEText(html_text, "html", "utf8")
|
||||
|
||||
plain_text = self._template_text.render(**template_vars)
|
||||
text_part = MIMEText(plain_text, "plain", "utf8")
|
||||
|
||||
for address in addresses:
|
||||
raw_to = email.utils.parseaddr(address)[1]
|
||||
|
||||
multipart_msg = MIMEMultipart('alternative')
|
||||
multipart_msg['Subject'] = self._subject
|
||||
multipart_msg['From'] = self._from_string
|
||||
multipart_msg['To'] = address
|
||||
multipart_msg['Date'] = email.utils.formatdate()
|
||||
multipart_msg['Message-ID'] = email.utils.make_msgid()
|
||||
multipart_msg.attach(text_part)
|
||||
multipart_msg.attach(html_part)
|
||||
|
||||
logger.info("Sending renewal email to %s", address)
|
||||
|
||||
yield make_deferred_yieldable(self.sendmail(
|
||||
self.hs.config.email_smtp_host,
|
||||
self._raw_from, raw_to, multipart_msg.as_string().encode('utf8'),
|
||||
reactor=self.hs.get_reactor(),
|
||||
port=self.hs.config.email_smtp_port,
|
||||
requireAuthentication=self.hs.config.email_smtp_user is not None,
|
||||
username=self.hs.config.email_smtp_user,
|
||||
password=self.hs.config.email_smtp_pass,
|
||||
requireTransportSecurity=self.hs.config.require_transport_security
|
||||
))
|
||||
|
||||
yield self.store.set_renewal_mail_status(
|
||||
user_id=user_id,
|
||||
email_sent=True,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_email_addresses_for_user(self, user_id):
|
||||
"""Retrieve the list of email addresses attached to a user's account.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to lookup email addresses for.
|
||||
|
||||
Returns:
|
||||
defer.Deferred[list[str]]: Email addresses for this account.
|
||||
"""
|
||||
threepids = yield self.store.user_get_threepids(user_id)
|
||||
|
||||
addresses = []
|
||||
for threepid in threepids:
|
||||
if threepid["medium"] == "email":
|
||||
addresses.append(threepid["address"])
|
||||
|
||||
defer.returnValue(addresses)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_renewal_token(self, user_id):
|
||||
"""Generates a 32-byte long random string that will be inserted into the
|
||||
user's renewal email's unique link, then saves it into the database.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to generate a string for.
|
||||
|
||||
Returns:
|
||||
defer.Deferred[str]: The generated string.
|
||||
|
||||
Raises:
|
||||
StoreError(500): Couldn't generate a unique string after 5 attempts.
|
||||
"""
|
||||
attempts = 0
|
||||
while attempts < 5:
|
||||
try:
|
||||
renewal_token = stringutils.random_string(32)
|
||||
yield self.store.set_renewal_token_for_user(user_id, renewal_token)
|
||||
defer.returnValue(renewal_token)
|
||||
except StoreError:
|
||||
attempts += 1
|
||||
raise StoreError(500, "Couldn't generate a unique string as refresh string.")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def renew_account(self, renewal_token):
|
||||
"""Renews the account attached to a given renewal token by pushing back the
|
||||
expiration date by the current validity period in the server's configuration.
|
||||
|
||||
Args:
|
||||
renewal_token (str): Token sent with the renewal request.
|
||||
"""
|
||||
user_id = yield self.store.get_user_from_renewal_token(renewal_token)
|
||||
logger.debug("Renewing an account for user %s", user_id)
|
||||
yield self.renew_account_for_user(user_id)
|
||||
|
||||
@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
|
||||
expiration date by the current validity period in the server's
|
||||
configuration.
|
||||
|
||||
Args:
|
||||
renewal_token (str): Token sent with the renewal request.
|
||||
expiration_ts (int): New expiration date. Defaults to now + validity period.
|
||||
email_sent (bool): Whether an email has been sent for this validity period.
|
||||
Defaults to False.
|
||||
|
||||
Returns:
|
||||
defer.Deferred[int]: New expiration date for this account, as a timestamp
|
||||
in milliseconds since epoch.
|
||||
"""
|
||||
if expiration_ts is None:
|
||||
expiration_ts = self.clock.time_msec() + self._account_validity.period
|
||||
|
||||
yield self.store.set_account_validity_for_user(
|
||||
user_id=user_id,
|
||||
expiration_ts=expiration_ts,
|
||||
email_sent=email_sent,
|
||||
)
|
||||
|
||||
defer.returnValue(expiration_ts)
|
|
@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class DirectoryHandler(BaseHandler):
|
||||
MAX_ALIAS_LENGTH = 255
|
||||
|
||||
def __init__(self, hs):
|
||||
super(DirectoryHandler, self).__init__(hs)
|
||||
|
@ -43,8 +44,10 @@ class DirectoryHandler(BaseHandler):
|
|||
self.state = hs.get_state_handler()
|
||||
self.appservice_handler = hs.get_application_service_handler()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.store = hs.get_datastore()
|
||||
self.config = hs.config
|
||||
self.enable_room_list_search = hs.config.enable_room_list_search
|
||||
self.require_membership = hs.config.require_membership_for_aliases
|
||||
|
||||
self.federation = hs.get_federation_client()
|
||||
hs.get_federation_registry().register_query_handler(
|
||||
|
@ -83,7 +86,7 @@ class DirectoryHandler(BaseHandler):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def create_association(self, requester, room_alias, room_id, servers=None,
|
||||
send_event=True):
|
||||
send_event=True, check_membership=True):
|
||||
"""Attempt to create a new alias
|
||||
|
||||
Args:
|
||||
|
@ -93,6 +96,8 @@ class DirectoryHandler(BaseHandler):
|
|||
servers (list[str]|None): List of servers that others servers
|
||||
should try and join via
|
||||
send_event (bool): Whether to send an updated m.room.aliases event
|
||||
check_membership (bool): Whether to check if the user is in the room
|
||||
before the alias can be set (if the server's config requires it).
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
|
@ -100,6 +105,13 @@ class DirectoryHandler(BaseHandler):
|
|||
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
if len(room_alias.to_string()) > self.MAX_ALIAS_LENGTH:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Can't create aliases longer than %s characters" % self.MAX_ALIAS_LENGTH,
|
||||
Codes.INVALID_PARAM,
|
||||
)
|
||||
|
||||
service = requester.app_service
|
||||
if service:
|
||||
if not service.is_interested_in_alias(room_alias.to_string()):
|
||||
|
@ -108,6 +120,14 @@ class DirectoryHandler(BaseHandler):
|
|||
" this kind of alias.", errcode=Codes.EXCLUSIVE
|
||||
)
|
||||
else:
|
||||
if self.require_membership and check_membership:
|
||||
rooms_for_user = yield self.store.get_rooms_for_user(user_id)
|
||||
if room_id not in rooms_for_user:
|
||||
raise AuthError(
|
||||
403,
|
||||
"You must be in the room to create an alias for it",
|
||||
)
|
||||
|
||||
if not self.spam_checker.user_may_create_room_alias(user_id, room_alias):
|
||||
raise AuthError(
|
||||
403, "This user is not permitted to create this alias",
|
||||
|
|
|
@ -828,6 +828,11 @@ class PresenceHandler(object):
|
|||
if typ != EventTypes.Member:
|
||||
continue
|
||||
|
||||
if event_id is None:
|
||||
# state has been deleted, so this is not a join. We only care about
|
||||
# joins.
|
||||
continue
|
||||
|
||||
event = yield self.store.get_event(event_id)
|
||||
if event.content.get("membership") != Membership.JOIN:
|
||||
# We only care about joins
|
||||
|
|
|
@ -402,7 +402,7 @@ class RoomCreationHandler(BaseHandler):
|
|||
yield directory_handler.create_association(
|
||||
requester, RoomAlias.from_string(alias),
|
||||
new_room_id, servers=(self.hs.hostname, ),
|
||||
send_event=False,
|
||||
send_event=False, check_membership=False,
|
||||
)
|
||||
logger.info("Moved alias %s to new room", alias)
|
||||
except SynapseError as e:
|
||||
|
@ -538,6 +538,7 @@ class RoomCreationHandler(BaseHandler):
|
|||
room_alias=room_alias,
|
||||
servers=[self.hs.hostname],
|
||||
send_event=False,
|
||||
check_membership=False,
|
||||
)
|
||||
|
||||
preset_config = config.get(
|
||||
|
|
|
@ -73,6 +73,7 @@ class RoomMemberHandler(object):
|
|||
self.clock = hs.get_clock()
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self._server_notices_mxid = self.config.server_notices_mxid
|
||||
self._enable_lookup = hs.config.enable_3pid_lookup
|
||||
|
||||
# This is only used to get at ratelimit function, and
|
||||
# maybe_kick_guest_users. It's fine there are multiple of these as
|
||||
|
@ -768,6 +769,10 @@ 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,),
|
||||
|
|
|
@ -149,7 +149,7 @@ class MatrixFederationAgent(object):
|
|||
tls_options = None
|
||||
else:
|
||||
tls_options = self._tls_client_options_factory.get_options(
|
||||
res.tls_server_name.decode("ascii")
|
||||
res.tls_server_name.decode("ascii"),
|
||||
)
|
||||
|
||||
# make sure that the Host header is set correctly
|
||||
|
|
|
@ -261,6 +261,23 @@ BASE_APPEND_OVERRIDE_RULES = [
|
|||
'value': True,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'rule_id': 'global/override/.m.rule.tombstone',
|
||||
'conditions': [
|
||||
{
|
||||
'kind': 'event_match',
|
||||
'key': 'type',
|
||||
'pattern': 'm.room.tombstone',
|
||||
'_id': '_tombstone',
|
||||
}
|
||||
],
|
||||
'actions': [
|
||||
'notify', {
|
||||
'set_tweak': 'highlight',
|
||||
'value': True,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -521,11 +521,11 @@ def format_ts_filter(value, format):
|
|||
return time.strftime(format, time.localtime(value / 1000))
|
||||
|
||||
|
||||
def load_jinja2_templates(config):
|
||||
def load_jinja2_templates(config, template_html_name, template_text_name):
|
||||
"""Load the jinja2 email templates from disk
|
||||
|
||||
Returns:
|
||||
(notif_template_html, notif_template_text)
|
||||
(template_html, template_text)
|
||||
"""
|
||||
logger.info("loading email templates from '%s'", config.email_template_dir)
|
||||
loader = jinja2.FileSystemLoader(config.email_template_dir)
|
||||
|
@ -533,14 +533,10 @@ def load_jinja2_templates(config):
|
|||
env.filters["format_ts"] = format_ts_filter
|
||||
env.filters["mxc_to_http"] = _create_mxc_to_http_filter(config)
|
||||
|
||||
notif_template_html = env.get_template(
|
||||
config.email_notif_template_html
|
||||
)
|
||||
notif_template_text = env.get_template(
|
||||
config.email_notif_template_text
|
||||
)
|
||||
template_html = env.get_template(template_html_name)
|
||||
template_text = env.get_template(template_text_name)
|
||||
|
||||
return notif_template_html, notif_template_text
|
||||
return template_html, template_text
|
||||
|
||||
|
||||
def _create_mxc_to_http_filter(config):
|
||||
|
|
|
@ -44,7 +44,11 @@ class PusherFactory(object):
|
|||
if hs.config.email_enable_notifs:
|
||||
self.mailers = {} # app_name -> Mailer
|
||||
|
||||
templates = load_jinja2_templates(hs.config)
|
||||
templates = load_jinja2_templates(
|
||||
config=hs.config,
|
||||
template_html_name=hs.config.email_notif_template_html,
|
||||
template_text_name=hs.config.email_notif_template_text,
|
||||
)
|
||||
self.notif_template_html, self.notif_template_text = templates
|
||||
|
||||
self.pusher_types["email"] = self._create_email_pusher
|
||||
|
|
|
@ -74,7 +74,9 @@ REQUIREMENTS = [
|
|||
CONDITIONAL_REQUIREMENTS = {
|
||||
"email.enable_notifs": ["Jinja2>=2.9", "bleach>=1.4.2"],
|
||||
"matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],
|
||||
"postgres": ["psycopg2>=2.6"],
|
||||
|
||||
# we use execute_batch, which arrived in psycopg 2.7.
|
||||
"postgres": ["psycopg2>=2.7"],
|
||||
|
||||
# ConsentResource uses select_autoescape, which arrived in jinja 2.9
|
||||
"resources.consent": ["Jinja2>=2.9"],
|
||||
|
@ -84,18 +86,22 @@ CONDITIONAL_REQUIREMENTS = {
|
|||
"acme": ["txacme>=0.9.2"],
|
||||
|
||||
"saml2": ["pysaml2>=4.5.0"],
|
||||
"systemd": ["systemd-python>=231"],
|
||||
"url_preview": ["lxml>=3.5.0"],
|
||||
"test": ["mock>=2.0", "parameterized"],
|
||||
"sentry": ["sentry-sdk>=0.7.2"],
|
||||
}
|
||||
|
||||
ALL_OPTIONAL_REQUIREMENTS = set()
|
||||
|
||||
for name, optional_deps in CONDITIONAL_REQUIREMENTS.items():
|
||||
# Exclude systemd as it's a system-based requirement.
|
||||
if name not in ["systemd"]:
|
||||
ALL_OPTIONAL_REQUIREMENTS = set(optional_deps) | ALL_OPTIONAL_REQUIREMENTS
|
||||
|
||||
|
||||
def list_requirements():
|
||||
deps = set(REQUIREMENTS)
|
||||
for opt in CONDITIONAL_REQUIREMENTS.values():
|
||||
deps = set(opt) | deps
|
||||
|
||||
return list(deps)
|
||||
return list(set(REQUIREMENTS) | ALL_OPTIONAL_REQUIREMENTS)
|
||||
|
||||
|
||||
class DependencyException(Exception):
|
||||
|
|
|
@ -13,22 +13,9 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.storage import DataStore
|
||||
from synapse.storage.keys import KeyStore
|
||||
from synapse.storage import KeyStore
|
||||
|
||||
from ._base import BaseSlavedStore, __func__
|
||||
# KeyStore isn't really safe to use from a worker, but for now we do so and hope that
|
||||
# the races it creates aren't too bad.
|
||||
|
||||
|
||||
class SlavedKeyStore(BaseSlavedStore):
|
||||
_get_server_verify_key = KeyStore.__dict__[
|
||||
"_get_server_verify_key"
|
||||
]
|
||||
|
||||
get_server_verify_keys = __func__(DataStore.get_server_verify_keys)
|
||||
store_server_verify_key = __func__(DataStore.store_server_verify_key)
|
||||
|
||||
get_server_certificate = __func__(DataStore.get_server_certificate)
|
||||
store_server_certificate = __func__(DataStore.store_server_certificate)
|
||||
|
||||
get_server_keys_json = __func__(DataStore.get_server_keys_json)
|
||||
store_server_keys_json = __func__(DataStore.store_server_keys_json)
|
||||
SlavedKeyStore = KeyStore
|
||||
|
|
4
synapse/res/templates/mail-expiry.css
Normal file
4
synapse/res/templates/mail-expiry.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.noticetext {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
43
synapse/res/templates/notice_expiry.html
Normal file
43
synapse/res/templates/notice_expiry.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!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 %}
|
||||
{% include 'mail-expiry.css' without context %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table id="page">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td id="inner">
|
||||
<table class="header">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="salutation">Hi {{ display_name }},</div>
|
||||
</td>
|
||||
<td class="logo">
|
||||
{% if app_name == "Riot" %}
|
||||
<img src="http://riot.im/img/external/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>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="noticetext">Your account will expire on {{ expiration_ts|format_ts("%d-%m-%Y") }}. This means that you will lose access to your account after this date.</div>
|
||||
<div class="noticetext">To extend the validity of your account, please click on the link bellow (or copy and paste it into a new browser tab):</div>
|
||||
<div class="noticetext"><a href="{{ url }}">{{ url }}</a></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
7
synapse/res/templates/notice_expiry.txt
Normal file
7
synapse/res/templates/notice_expiry.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
Hi {{ display_name }},
|
||||
|
||||
Your account will expire on {{ expiration_ts|format_ts("%d-%m-%Y") }}. This means that you will lose access to your account after this date.
|
||||
|
||||
To extend the validity of your account, please click on the link bellow (or copy and paste it to a new browser tab):
|
||||
|
||||
{{ url }}
|
|
@ -13,11 +13,10 @@
|
|||
# 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 synapse.rest.admin
|
||||
from synapse.http.server import JsonResource
|
||||
from synapse.rest.client import versions
|
||||
from synapse.rest.client.v1 import (
|
||||
admin,
|
||||
directory,
|
||||
events,
|
||||
initial_sync,
|
||||
|
@ -33,6 +32,7 @@ from synapse.rest.client.v1 import (
|
|||
from synapse.rest.client.v2_alpha import (
|
||||
account,
|
||||
account_data,
|
||||
account_validity,
|
||||
auth,
|
||||
capabilities,
|
||||
devices,
|
||||
|
@ -57,8 +57,14 @@ from synapse.rest.client.v2_alpha import (
|
|||
|
||||
|
||||
class ClientRestResource(JsonResource):
|
||||
"""A resource for version 1 of the matrix client API."""
|
||||
"""Matrix Client API REST resource.
|
||||
|
||||
This gets mounted at various points under /_matrix/client, including:
|
||||
* /_matrix/client/r0
|
||||
* /_matrix/client/api/v1
|
||||
* /_matrix/client/unstable
|
||||
* etc
|
||||
"""
|
||||
def __init__(self, hs):
|
||||
JsonResource.__init__(self, hs, canonical_json=False)
|
||||
self.register_servlets(self, hs)
|
||||
|
@ -81,7 +87,6 @@ class ClientRestResource(JsonResource):
|
|||
presence.register_servlets(hs, client_resource)
|
||||
directory.register_servlets(hs, client_resource)
|
||||
voip.register_servlets(hs, client_resource)
|
||||
admin.register_servlets(hs, client_resource)
|
||||
pusher.register_servlets(hs, client_resource)
|
||||
push_rule.register_servlets(hs, client_resource)
|
||||
logout.register_servlets(hs, client_resource)
|
||||
|
@ -109,3 +114,9 @@ class ClientRestResource(JsonResource):
|
|||
groups.register_servlets(hs, client_resource)
|
||||
room_upgrade_rest_servlet.register_servlets(hs, client_resource)
|
||||
capabilities.register_servlets(hs, client_resource)
|
||||
account_validity.register_servlets(hs, client_resource)
|
||||
|
||||
# moving to /_synapse/admin
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource(
|
||||
hs, client_resource
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket 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.
|
||||
|
@ -18,6 +18,7 @@ import hashlib
|
|||
import hmac
|
||||
import logging
|
||||
import platform
|
||||
import re
|
||||
|
||||
from six import text_type
|
||||
from six.moves import http_client
|
||||
|
@ -27,39 +28,56 @@ from twisted.internet import defer
|
|||
import synapse
|
||||
from synapse.api.constants import Membership, UserTypes
|
||||
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
|
||||
from synapse.http.server import JsonResource
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
assert_params_in_dict,
|
||||
parse_integer,
|
||||
parse_json_object_from_request,
|
||||
parse_string,
|
||||
)
|
||||
from synapse.rest.admin._base import assert_requester_is_admin, assert_user_is_admin
|
||||
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
|
||||
from synapse.types import UserID, create_requester
|
||||
from synapse.util.versionstring import get_version_string
|
||||
|
||||
from .base import ClientV1RestServlet, client_path_patterns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UsersRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/admin/users/(?P<user_id>[^/]*)")
|
||||
def historical_admin_path_patterns(path_regex):
|
||||
"""Returns the list of patterns for an admin endpoint, including historical ones
|
||||
|
||||
This is a backwards-compatibility hack. Previously, the Admin API was exposed at
|
||||
various paths under /_matrix/client. This function returns a list of patterns
|
||||
matching those paths (as well as the new one), so that existing scripts which rely
|
||||
on the endpoints being available there are not broken.
|
||||
|
||||
Note that this should only be used for existing endpoints: new ones should just
|
||||
register for the /_synapse/admin path.
|
||||
"""
|
||||
return list(
|
||||
re.compile(prefix + path_regex)
|
||||
for prefix in (
|
||||
"^/_synapse/admin/v1",
|
||||
"^/_matrix/client/api/v1/admin",
|
||||
"^/_matrix/client/unstable/admin",
|
||||
"^/_matrix/client/r0/admin"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class UsersRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/users/(?P<user_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(UsersRestServlet, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.handlers = hs.get_handlers()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, user_id):
|
||||
target_user = UserID.from_string(user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
|
||||
# To allow all users to get the users list
|
||||
# if not is_admin and target_user != auth_user:
|
||||
# raise AuthError(403, "You are not a server admin")
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "Can only users a local user")
|
||||
|
@ -69,16 +87,15 @@ class UsersRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class VersionServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/admin/server_version")
|
||||
class VersionServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/server_version")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
ret = {
|
||||
'server_version': get_version_string(synapse),
|
||||
|
@ -88,18 +105,17 @@ class VersionServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class UserRegisterServlet(ClientV1RestServlet):
|
||||
class UserRegisterServlet(RestServlet):
|
||||
"""
|
||||
Attributes:
|
||||
NONCE_TIMEOUT (int): Seconds until a generated nonce won't be accepted
|
||||
nonces (dict[str, int]): The nonces that we will accept. A dict of
|
||||
nonce to the time it was generated, in int seconds.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/register")
|
||||
PATTERNS = historical_admin_path_patterns("/register")
|
||||
NONCE_TIMEOUT = 60
|
||||
|
||||
def __init__(self, hs):
|
||||
super(UserRegisterServlet, self).__init__(hs)
|
||||
self.handlers = hs.get_handlers()
|
||||
self.reactor = hs.get_reactor()
|
||||
self.nonces = {}
|
||||
|
@ -226,11 +242,12 @@ class UserRegisterServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, result))
|
||||
|
||||
|
||||
class WhoisRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/admin/whois/(?P<user_id>[^/]*)")
|
||||
class WhoisRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/whois/(?P<user_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(WhoisRestServlet, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.handlers = hs.get_handlers()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
@ -238,10 +255,9 @@ class WhoisRestServlet(ClientV1RestServlet):
|
|||
target_user = UserID.from_string(user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
auth_user = requester.user
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin and target_user != auth_user:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
if target_user != auth_user:
|
||||
yield assert_user_is_admin(self.auth, auth_user)
|
||||
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "Can only whois a local user")
|
||||
|
@ -251,20 +267,16 @@ class WhoisRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class PurgeMediaCacheRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/admin/purge_media_cache")
|
||||
class PurgeMediaCacheRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/purge_media_cache")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.media_repository = hs.get_media_repository()
|
||||
super(PurgeMediaCacheRestServlet, self).__init__(hs)
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
before_ts = parse_integer(request, "before_ts", required=True)
|
||||
logger.info("before_ts: %r", before_ts)
|
||||
|
@ -274,9 +286,9 @@ class PurgeMediaCacheRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class PurgeHistoryRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns(
|
||||
"/admin/purge_history/(?P<room_id>[^/]*)(/(?P<event_id>[^/]+))?"
|
||||
class PurgeHistoryRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns(
|
||||
"/purge_history/(?P<room_id>[^/]*)(/(?P<event_id>[^/]+))?"
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
|
@ -285,17 +297,13 @@ class PurgeHistoryRestServlet(ClientV1RestServlet):
|
|||
Args:
|
||||
hs (synapse.server.HomeServer)
|
||||
"""
|
||||
super(PurgeHistoryRestServlet, self).__init__(hs)
|
||||
self.pagination_handler = hs.get_pagination_handler()
|
||||
self.store = hs.get_datastore()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, room_id, event_id):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
body = parse_json_object_from_request(request, allow_empty_body=True)
|
||||
|
||||
|
@ -371,9 +379,9 @@ class PurgeHistoryRestServlet(ClientV1RestServlet):
|
|||
}))
|
||||
|
||||
|
||||
class PurgeHistoryStatusRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns(
|
||||
"/admin/purge_history_status/(?P<purge_id>[^/]+)"
|
||||
class PurgeHistoryStatusRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns(
|
||||
"/purge_history_status/(?P<purge_id>[^/]+)"
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
|
@ -382,16 +390,12 @@ class PurgeHistoryStatusRestServlet(ClientV1RestServlet):
|
|||
Args:
|
||||
hs (synapse.server.HomeServer)
|
||||
"""
|
||||
super(PurgeHistoryStatusRestServlet, self).__init__(hs)
|
||||
self.pagination_handler = hs.get_pagination_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, purge_id):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
purge_status = self.pagination_handler.get_purge_status(purge_id)
|
||||
if purge_status is None:
|
||||
|
@ -400,15 +404,16 @@ class PurgeHistoryStatusRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, purge_status.asdict()))
|
||||
|
||||
|
||||
class DeactivateAccountRestServlet(ClientV1RestServlet):
|
||||
PATTERNS = client_path_patterns("/admin/deactivate/(?P<target_user_id>[^/]*)")
|
||||
class DeactivateAccountRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/deactivate/(?P<target_user_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(DeactivateAccountRestServlet, self).__init__(hs)
|
||||
self._deactivate_account_handler = hs.get_deactivate_account_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, target_user_id):
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
body = parse_json_object_from_request(request, allow_empty_body=True)
|
||||
erase = body.get("erase", False)
|
||||
if not isinstance(erase, bool):
|
||||
|
@ -419,11 +424,6 @@ class DeactivateAccountRestServlet(ClientV1RestServlet):
|
|||
)
|
||||
|
||||
UserID.from_string(target_user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
|
||||
result = yield self._deactivate_account_handler.deactivate_account(
|
||||
target_user_id, erase,
|
||||
|
@ -438,13 +438,13 @@ class DeactivateAccountRestServlet(ClientV1RestServlet):
|
|||
}))
|
||||
|
||||
|
||||
class ShutdownRoomRestServlet(ClientV1RestServlet):
|
||||
class ShutdownRoomRestServlet(RestServlet):
|
||||
"""Shuts down a room by removing all local users from the room and blocking
|
||||
all future invites and joins to the room. Any local aliases will be repointed
|
||||
to a new room created by `new_room_user_id` and kicked users will be auto
|
||||
joined to the new room.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/shutdown_room/(?P<room_id>[^/]+)")
|
||||
PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P<room_id>[^/]+)")
|
||||
|
||||
DEFAULT_MESSAGE = (
|
||||
"Sharing illegal content on this server is not permitted and rooms in"
|
||||
|
@ -452,19 +452,18 @@ class ShutdownRoomRestServlet(ClientV1RestServlet):
|
|||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ShutdownRoomRestServlet, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastore()
|
||||
self.state = hs.get_state_handler()
|
||||
self._room_creation_handler = hs.get_room_creation_handler()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, room_id):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
content = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(content, ["new_room_user_id"])
|
||||
|
@ -564,22 +563,20 @@ class ShutdownRoomRestServlet(ClientV1RestServlet):
|
|||
}))
|
||||
|
||||
|
||||
class QuarantineMediaInRoom(ClientV1RestServlet):
|
||||
class QuarantineMediaInRoom(RestServlet):
|
||||
"""Quarantines all media in a room so that no one can download it via
|
||||
this server.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/quarantine_media/(?P<room_id>[^/]+)")
|
||||
PATTERNS = historical_admin_path_patterns("/quarantine_media/(?P<room_id>[^/]+)")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(QuarantineMediaInRoom, self).__init__(hs)
|
||||
self.store = hs.get_datastore()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, room_id):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
num_quarantined = yield self.store.quarantine_media_ids_in_room(
|
||||
room_id, requester.user.to_string(),
|
||||
|
@ -588,13 +585,12 @@ class QuarantineMediaInRoom(ClientV1RestServlet):
|
|||
defer.returnValue((200, {"num_quarantined": num_quarantined}))
|
||||
|
||||
|
||||
class ListMediaInRoom(ClientV1RestServlet):
|
||||
class ListMediaInRoom(RestServlet):
|
||||
"""Lists all of the media in a given room.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/room/(?P<room_id>[^/]+)/media")
|
||||
PATTERNS = historical_admin_path_patterns("/room/(?P<room_id>[^/]+)/media")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ListMediaInRoom, self).__init__(hs)
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
@ -609,11 +605,11 @@ class ListMediaInRoom(ClientV1RestServlet):
|
|||
defer.returnValue((200, {"local": local_mxcs, "remote": remote_mxcs}))
|
||||
|
||||
|
||||
class ResetPasswordRestServlet(ClientV1RestServlet):
|
||||
class ResetPasswordRestServlet(RestServlet):
|
||||
"""Post request to allow an administrator reset password for a user.
|
||||
This needs user to have administrator access in Synapse.
|
||||
Example:
|
||||
http://localhost:8008/_matrix/client/api/v1/admin/reset_password/
|
||||
http://localhost:8008/_synapse/admin/v1/reset_password/
|
||||
@user:to_reset_password?access_token=admin_access_token
|
||||
JsonBodyToSend:
|
||||
{
|
||||
|
@ -622,11 +618,10 @@ class ResetPasswordRestServlet(ClientV1RestServlet):
|
|||
Returns:
|
||||
200 OK with empty object if success otherwise an error.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/reset_password/(?P<target_user_id>[^/]*)")
|
||||
PATTERNS = historical_admin_path_patterns("/reset_password/(?P<target_user_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
super(ResetPasswordRestServlet, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self._set_password_handler = hs.get_set_password_handler()
|
||||
|
@ -636,12 +631,10 @@ class ResetPasswordRestServlet(ClientV1RestServlet):
|
|||
"""Post request to allow an administrator reset password for a user.
|
||||
This needs user to have administrator access in Synapse.
|
||||
"""
|
||||
UserID.from_string(target_user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
yield assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
UserID.from_string(target_user_id)
|
||||
|
||||
params = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(params, ["new_password"])
|
||||
|
@ -653,20 +646,19 @@ class ResetPasswordRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, {}))
|
||||
|
||||
|
||||
class GetUsersPaginatedRestServlet(ClientV1RestServlet):
|
||||
class GetUsersPaginatedRestServlet(RestServlet):
|
||||
"""Get request to get specific number of users from Synapse.
|
||||
This needs user to have administrator access in Synapse.
|
||||
Example:
|
||||
http://localhost:8008/_matrix/client/api/v1/admin/users_paginate/
|
||||
http://localhost:8008/_synapse/admin/v1/users_paginate/
|
||||
@admin:user?access_token=admin_access_token&start=0&limit=10
|
||||
Returns:
|
||||
200 OK with json object {list[dict[str, Any]], count} or empty object.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/users_paginate/(?P<target_user_id>[^/]*)")
|
||||
PATTERNS = historical_admin_path_patterns("/users_paginate/(?P<target_user_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
super(GetUsersPaginatedRestServlet, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.handlers = hs.get_handlers()
|
||||
|
@ -676,16 +668,9 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
|
|||
"""Get request to get specific number of users from Synapse.
|
||||
This needs user to have administrator access in Synapse.
|
||||
"""
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
target_user = UserID.from_string(target_user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
|
||||
# To allow all users to get the users list
|
||||
# if not is_admin and target_user != auth_user:
|
||||
# raise AuthError(403, "You are not a server admin")
|
||||
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "Can only users a local user")
|
||||
|
@ -706,7 +691,7 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
|
|||
"""Post request to get specific number of users from Synapse..
|
||||
This needs user to have administrator access in Synapse.
|
||||
Example:
|
||||
http://localhost:8008/_matrix/client/api/v1/admin/users_paginate/
|
||||
http://localhost:8008/_synapse/admin/v1/users_paginate/
|
||||
@admin:user?access_token=admin_access_token
|
||||
JsonBodyToSend:
|
||||
{
|
||||
|
@ -716,12 +701,8 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
|
|||
Returns:
|
||||
200 OK with json object {list[dict[str, Any]], count} or empty object.
|
||||
"""
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
UserID.from_string(target_user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
|
||||
order = "name" # order by name in user table
|
||||
params = parse_json_object_from_request(request)
|
||||
|
@ -736,21 +717,20 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class SearchUsersRestServlet(ClientV1RestServlet):
|
||||
class SearchUsersRestServlet(RestServlet):
|
||||
"""Get request to search user table for specific users according to
|
||||
search term.
|
||||
This needs user to have administrator access in Synapse.
|
||||
Example:
|
||||
http://localhost:8008/_matrix/client/api/v1/admin/search_users/
|
||||
http://localhost:8008/_synapse/admin/v1/search_users/
|
||||
@admin:user?access_token=admin_access_token&term=alice
|
||||
Returns:
|
||||
200 OK with json object {list[dict[str, Any]], count} or empty object.
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/search_users/(?P<target_user_id>[^/]*)")
|
||||
PATTERNS = historical_admin_path_patterns("/search_users/(?P<target_user_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
super(SearchUsersRestServlet, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.handlers = hs.get_handlers()
|
||||
|
@ -761,12 +741,9 @@ class SearchUsersRestServlet(ClientV1RestServlet):
|
|||
search term.
|
||||
This needs user to have a administrator access in Synapse.
|
||||
"""
|
||||
target_user = UserID.from_string(target_user_id)
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
target_user = UserID.from_string(target_user_id)
|
||||
|
||||
# To allow all users to get the users list
|
||||
# if not is_admin and target_user != auth_user:
|
||||
|
@ -784,23 +761,20 @@ class SearchUsersRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class DeleteGroupAdminRestServlet(ClientV1RestServlet):
|
||||
class DeleteGroupAdminRestServlet(RestServlet):
|
||||
"""Allows deleting of local groups
|
||||
"""
|
||||
PATTERNS = client_path_patterns("/admin/delete_group/(?P<group_id>[^/]*)")
|
||||
PATTERNS = historical_admin_path_patterns("/delete_group/(?P<group_id>[^/]*)")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(DeleteGroupAdminRestServlet, self).__init__(hs)
|
||||
self.group_server = hs.get_groups_server_handler()
|
||||
self.is_mine_id = hs.is_mine_id
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, group_id):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
is_admin = yield self.auth.is_server_admin(requester.user)
|
||||
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
||||
yield assert_user_is_admin(self.auth, requester.user)
|
||||
|
||||
if not self.is_mine_id(group_id):
|
||||
raise SynapseError(400, "Can only delete local groups")
|
||||
|
@ -809,7 +783,57 @@ class DeleteGroupAdminRestServlet(ClientV1RestServlet):
|
|||
defer.returnValue((200, {}))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
class AccountValidityRenewServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/account_validity/validity$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
self.hs = hs
|
||||
self.account_activity_handler = hs.get_account_validity_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if "user_id" not in body:
|
||||
raise SynapseError(400, "Missing property 'user_id' in the request body")
|
||||
|
||||
expiration_ts = yield self.account_activity_handler.renew_account_for_user(
|
||||
body["user_id"], body.get("expiration_ts"),
|
||||
not body.get("enable_renewal_emails", True),
|
||||
)
|
||||
|
||||
res = {
|
||||
"expiration_ts": expiration_ts,
|
||||
}
|
||||
defer.returnValue((200, res))
|
||||
|
||||
########################################################################################
|
||||
#
|
||||
# please don't add more servlets here: this file is already long and unwieldy. Put
|
||||
# them in separate files within the 'admin' package.
|
||||
#
|
||||
########################################################################################
|
||||
|
||||
|
||||
class AdminRestResource(JsonResource):
|
||||
"""The REST resource which gets mounted at /_synapse/admin"""
|
||||
|
||||
def __init__(self, hs):
|
||||
JsonResource.__init__(self, hs, canonical_json=False)
|
||||
|
||||
register_servlets_for_client_rest_resource(hs, self)
|
||||
SendServerNoticeServlet(hs).register(self)
|
||||
|
||||
|
||||
def register_servlets_for_client_rest_resource(hs, http_server):
|
||||
"""Register only the servlets which need to be exposed on /_matrix/client/xxx"""
|
||||
WhoisRestServlet(hs).register(http_server)
|
||||
PurgeMediaCacheRestServlet(hs).register(http_server)
|
||||
PurgeHistoryStatusRestServlet(hs).register(http_server)
|
||||
|
@ -825,3 +849,6 @@ def register_servlets(hs, http_server):
|
|||
UserRegisterServlet(hs).register(http_server)
|
||||
VersionServlet(hs).register(http_server)
|
||||
DeleteGroupAdminRestServlet(hs).register(http_server)
|
||||
AccountValidityRenewServlet(hs).register(http_server)
|
||||
# don't add more things here: new servlets should only be exposed on
|
||||
# /_synapse/admin so should not go here. Instead register them in AdminRestResource.
|
59
synapse/rest/admin/_base.py
Normal file
59
synapse/rest/admin/_base.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# -*- 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.
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import AuthError
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def assert_requester_is_admin(auth, request):
|
||||
"""Verify that the requester is an admin user
|
||||
|
||||
WARNING: MAKE SURE YOU YIELD ON THE RESULT!
|
||||
|
||||
Args:
|
||||
auth (synapse.api.auth.Auth):
|
||||
request (twisted.web.server.Request): incoming request
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
|
||||
Raises:
|
||||
AuthError if the requester is not an admin
|
||||
"""
|
||||
requester = yield auth.get_user_by_req(request)
|
||||
yield assert_user_is_admin(auth, requester.user)
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def assert_user_is_admin(auth, user_id):
|
||||
"""Verify that the given user is an admin user
|
||||
|
||||
WARNING: MAKE SURE YOU YIELD ON THE RESULT!
|
||||
|
||||
Args:
|
||||
auth (synapse.api.auth.Auth):
|
||||
user_id (UserID):
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
|
||||
Raises:
|
||||
AuthError if the user is not an admin
|
||||
"""
|
||||
|
||||
is_admin = yield auth.is_server_admin(user_id)
|
||||
if not is_admin:
|
||||
raise AuthError(403, "You are not a server admin")
|
100
synapse/rest/admin/server_notice_servlet.py
Normal file
100
synapse/rest/admin/server_notice_servlet.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
# -*- 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.
|
||||
import re
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
assert_params_in_dict,
|
||||
parse_json_object_from_request,
|
||||
)
|
||||
from synapse.rest.admin import assert_requester_is_admin
|
||||
from synapse.rest.client.transactions import HttpTransactionCache
|
||||
from synapse.types import UserID
|
||||
|
||||
|
||||
class SendServerNoticeServlet(RestServlet):
|
||||
"""Servlet which will send a server notice to a given user
|
||||
|
||||
POST /_synapse/admin/v1/send_server_notice
|
||||
{
|
||||
"user_id": "@target_user:server_name",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "This is my message"
|
||||
}
|
||||
}
|
||||
|
||||
returns:
|
||||
|
||||
{
|
||||
"event_id": "$1895723857jgskldgujpious"
|
||||
}
|
||||
"""
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.txns = HttpTransactionCache(hs)
|
||||
self.snm = hs.get_server_notices_manager()
|
||||
|
||||
def register(self, json_resource):
|
||||
PATTERN = "^/_synapse/admin/v1/send_server_notice"
|
||||
json_resource.register_paths(
|
||||
"POST",
|
||||
(re.compile(PATTERN + "$"), ),
|
||||
self.on_POST,
|
||||
)
|
||||
json_resource.register_paths(
|
||||
"PUT",
|
||||
(re.compile(PATTERN + "/(?P<txn_id>[^/]*)$",), ),
|
||||
self.on_PUT,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, txn_id=None):
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
body = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(body, ("user_id", "content"))
|
||||
event_type = body.get("type", EventTypes.Message)
|
||||
state_key = body.get("state_key")
|
||||
|
||||
if not self.snm.is_enabled():
|
||||
raise SynapseError(400, "Server notices are not enabled on this server")
|
||||
|
||||
user_id = body["user_id"]
|
||||
UserID.from_string(user_id)
|
||||
if not self.hs.is_mine_id(user_id):
|
||||
raise SynapseError(400, "Server notices can only be sent to local users")
|
||||
|
||||
event = yield self.snm.send_notice(
|
||||
user_id=body["user_id"],
|
||||
type=event_type,
|
||||
state_key=state_key,
|
||||
event_content=body["content"],
|
||||
)
|
||||
|
||||
defer.returnValue((200, {"event_id": event.event_id}))
|
||||
|
||||
def on_PUT(self, request, txn_id):
|
||||
return self.txns.fetch_or_execute_request(
|
||||
request, self.on_POST, request, txn_id,
|
||||
)
|
91
synapse/rest/client/v2_alpha/account_validity.py
Normal file
91
synapse/rest/client/v2_alpha/account_validity.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
# -*- 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.
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import AuthError, SynapseError
|
||||
from synapse.http.server import finish_request
|
||||
from synapse.http.servlet import RestServlet
|
||||
|
||||
from ._base import client_v2_patterns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountValidityRenewServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/account_validity/renew$")
|
||||
SUCCESS_HTML = b"<html><body>Your account has been successfully renewed.</body><html>"
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(AccountValidityRenewServlet, self).__init__()
|
||||
|
||||
self.hs = hs
|
||||
self.account_activity_handler = hs.get_account_validity_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
if b"token" not in request.args:
|
||||
raise SynapseError(400, "Missing renewal token")
|
||||
renewal_token = request.args[b"token"][0]
|
||||
|
||||
yield self.account_activity_handler.renew_account(renewal_token.decode('utf8'))
|
||||
|
||||
request.setResponseCode(200)
|
||||
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)
|
||||
finish_request(request)
|
||||
defer.returnValue(None)
|
||||
|
||||
|
||||
class AccountValiditySendMailServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/account_validity/send_mail$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(AccountValiditySendMailServlet, self).__init__()
|
||||
|
||||
self.hs = hs
|
||||
self.account_activity_handler = hs.get_account_validity_handler()
|
||||
self.auth = hs.get_auth()
|
||||
self.account_validity = self.hs.config.account_validity
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
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)
|
||||
user_id = requester.user.to_string()
|
||||
yield self.account_activity_handler.send_renewal_email_to_user(user_id)
|
||||
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
AccountValidityRenewServlet(hs).register(http_server)
|
||||
AccountValiditySendMailServlet(hs).register(http_server)
|
|
@ -391,6 +391,13 @@ class RegisterRestServlet(RestServlet):
|
|||
# the user-facing checks will probably already have happened in
|
||||
# /register/email/requestToken when we requested a 3pid, but that's not
|
||||
# guaranteed.
|
||||
#
|
||||
# Also check that we're not trying to register a 3pid that's already
|
||||
# been registered.
|
||||
#
|
||||
# This has probably happened in /register/email/requestToken as well,
|
||||
# but if a user hits this endpoint twice then clicks on each link from
|
||||
# the two activation emails, they would register the same 3pid twice.
|
||||
|
||||
if auth_result:
|
||||
for login_type in [LoginType.EMAIL_IDENTITY, LoginType.MSISDN]:
|
||||
|
@ -406,6 +413,17 @@ 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 registered_user_id is not None:
|
||||
logger.info(
|
||||
"Already registered user ID %r for this session",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2019 New Vector Ltd.
|
||||
# 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.
|
||||
|
@ -191,6 +191,10 @@ def respond_with_responder(request, responder, media_type, file_size, upload_nam
|
|||
# in that case.
|
||||
logger.warning("Failed to write to consumer: %s %s", type(e), e)
|
||||
|
||||
# Unregister the producer, if it has one, so Twisted doesn't complain
|
||||
if request.producer:
|
||||
request.unregisterProducer()
|
||||
|
||||
finish_request(request)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector 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.
|
||||
|
@ -68,6 +68,6 @@ class WellKnownResource(Resource):
|
|||
request.setHeader(b"Content-Type", b"text/plain")
|
||||
return b'.well-known not available'
|
||||
|
||||
logger.error("returning: %s", r)
|
||||
logger.debug("returning: %s", r)
|
||||
request.setHeader(b"Content-Type", b"application/json")
|
||||
return json.dumps(r).encode("utf-8")
|
||||
|
|
|
@ -47,6 +47,7 @@ from synapse.federation.transport.client import TransportLayerClient
|
|||
from synapse.groups.attestations import GroupAttestationSigning, GroupAttestionRenewer
|
||||
from synapse.groups.groups_server import GroupsServerHandler
|
||||
from synapse.handlers import Handlers
|
||||
from synapse.handlers.account_validity import AccountValidityHandler
|
||||
from synapse.handlers.acme import AcmeHandler
|
||||
from synapse.handlers.appservice import ApplicationServicesHandler
|
||||
from synapse.handlers.auth import AuthHandler, MacaroonGenerator
|
||||
|
@ -183,6 +184,7 @@ class HomeServer(object):
|
|||
'room_context_handler',
|
||||
'sendmail',
|
||||
'registration_handler',
|
||||
'account_validity_handler',
|
||||
]
|
||||
|
||||
REQUIRED_ON_MASTER_STARTUP = [
|
||||
|
@ -506,6 +508,9 @@ class HomeServer(object):
|
|||
def build_registration_handler(self):
|
||||
return RegistrationHandler(self)
|
||||
|
||||
def build_account_validity_handler(self):
|
||||
return AccountValidityHandler(self)
|
||||
|
||||
def remove_pusher(self, app_id, push_key, user_id):
|
||||
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)
|
||||
|
||||
|
|
|
@ -1179,14 +1179,10 @@ class EventsStore(
|
|||
"events",
|
||||
"event_auth",
|
||||
"event_json",
|
||||
"event_content_hashes",
|
||||
"event_destinations",
|
||||
"event_edge_hashes",
|
||||
"event_edges",
|
||||
"event_forward_extremities",
|
||||
"event_reference_hashes",
|
||||
"event_search",
|
||||
"event_signatures",
|
||||
"event_to_state_groups",
|
||||
"guest_access",
|
||||
"history_visibility",
|
||||
|
@ -1857,16 +1853,12 @@ class EventsStore(
|
|||
# Tables that should be pruned:
|
||||
# event_auth
|
||||
# event_backward_extremities
|
||||
# event_content_hashes
|
||||
# event_destinations
|
||||
# event_edge_hashes
|
||||
# event_edges
|
||||
# event_forward_extremities
|
||||
# event_json
|
||||
# event_push_actions
|
||||
# event_reference_hashes
|
||||
# event_search
|
||||
# event_signatures
|
||||
# event_to_state_groups
|
||||
# events
|
||||
# rejections
|
||||
|
@ -2065,14 +2057,10 @@ class EventsStore(
|
|||
"events",
|
||||
"event_json",
|
||||
"event_auth",
|
||||
"event_content_hashes",
|
||||
"event_destinations",
|
||||
"event_edge_hashes",
|
||||
"event_edges",
|
||||
"event_forward_extremities",
|
||||
"event_reference_hashes",
|
||||
"event_search",
|
||||
"event_signatures",
|
||||
"rejections",
|
||||
):
|
||||
logger.info("[purge] removing events from %s", table)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# 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.
|
||||
|
@ -13,17 +14,15 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import hashlib
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
|
||||
import OpenSSL
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.util.caches.descriptors import cachedInlineCallbacks
|
||||
from synapse.util import batch_iter
|
||||
from synapse.util.caches.descriptors import cached, cachedList
|
||||
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
|
@ -38,83 +37,52 @@ else:
|
|||
|
||||
|
||||
class KeyStore(SQLBaseStore):
|
||||
"""Persistence for signature verification keys and tls X.509 certificates
|
||||
"""Persistence for signature verification keys
|
||||
"""
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_server_certificate(self, server_name):
|
||||
"""Retrieve the TLS X.509 certificate for the given server
|
||||
Args:
|
||||
server_name (bytes): The name of the server.
|
||||
Returns:
|
||||
(OpenSSL.crypto.X509): The tls certificate.
|
||||
@cached()
|
||||
def _get_server_verify_key(self, server_name_and_key_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
@cachedList(
|
||||
cached_method_name="_get_server_verify_key", list_name="server_name_and_key_ids"
|
||||
)
|
||||
def get_server_verify_keys(self, server_name_and_key_ids):
|
||||
"""
|
||||
tls_certificate_bytes, = yield self._simple_select_one(
|
||||
table="server_tls_certificates",
|
||||
keyvalues={"server_name": server_name},
|
||||
retcols=("tls_certificate",),
|
||||
desc="get_server_certificate",
|
||||
)
|
||||
tls_certificate = OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, tls_certificate_bytes
|
||||
)
|
||||
defer.returnValue(tls_certificate)
|
||||
|
||||
def store_server_certificate(
|
||||
self, server_name, from_server, time_now_ms, tls_certificate
|
||||
):
|
||||
"""Stores the TLS X.509 certificate for the given server
|
||||
Args:
|
||||
server_name (str): The name of the server.
|
||||
from_server (str): Where the certificate was looked up
|
||||
time_now_ms (int): The time now in milliseconds
|
||||
tls_certificate (OpenSSL.crypto.X509): The X.509 certificate.
|
||||
"""
|
||||
tls_certificate_bytes = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, tls_certificate
|
||||
)
|
||||
fingerprint = hashlib.sha256(tls_certificate_bytes).hexdigest()
|
||||
return self._simple_upsert(
|
||||
table="server_tls_certificates",
|
||||
keyvalues={"server_name": server_name, "fingerprint": fingerprint},
|
||||
values={
|
||||
"from_server": from_server,
|
||||
"ts_added_ms": time_now_ms,
|
||||
"tls_certificate": db_binary_type(tls_certificate_bytes),
|
||||
},
|
||||
desc="store_server_certificate",
|
||||
)
|
||||
server_name_and_key_ids (iterable[Tuple[str, str]]):
|
||||
iterable of (server_name, key-id) tuples to fetch keys for
|
||||
|
||||
@cachedInlineCallbacks()
|
||||
def _get_server_verify_key(self, server_name, key_id):
|
||||
verify_key_bytes = yield self._simple_select_one_onecol(
|
||||
table="server_signature_keys",
|
||||
keyvalues={"server_name": server_name, "key_id": key_id},
|
||||
retcol="verify_key",
|
||||
desc="_get_server_verify_key",
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
if verify_key_bytes:
|
||||
defer.returnValue(decode_verify_key_bytes(key_id, bytes(verify_key_bytes)))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_server_verify_keys(self, server_name, key_ids):
|
||||
"""Retrieve the NACL verification key for a given server for the given
|
||||
key_ids
|
||||
Args:
|
||||
server_name (str): The name of the server.
|
||||
key_ids (iterable[str]): key_ids to try and look up.
|
||||
Returns:
|
||||
Deferred: resolves to dict[str, VerifyKey]: map from
|
||||
key_id to verification key.
|
||||
Deferred: resolves to dict[Tuple[str, str], VerifyKey|None]:
|
||||
map from (server_name, key_id) -> VerifyKey, or None if the key is
|
||||
unknown
|
||||
"""
|
||||
keys = {}
|
||||
for key_id in key_ids:
|
||||
key = yield self._get_server_verify_key(server_name, key_id)
|
||||
if key:
|
||||
keys[key_id] = key
|
||||
defer.returnValue(keys)
|
||||
|
||||
def _get_keys(txn, batch):
|
||||
"""Processes a batch of keys to fetch, and adds the result to `keys`."""
|
||||
|
||||
# batch_iter always returns tuples so it's safe to do len(batch)
|
||||
sql = (
|
||||
"SELECT server_name, key_id, verify_key FROM server_signature_keys "
|
||||
"WHERE 1=0"
|
||||
) + " OR (server_name=? AND key_id=?)" * len(batch)
|
||||
|
||||
txn.execute(sql, tuple(itertools.chain.from_iterable(batch)))
|
||||
|
||||
for row in txn:
|
||||
server_name, key_id, key_bytes = row
|
||||
keys[(server_name, key_id)] = decode_verify_key_bytes(
|
||||
key_id, bytes(key_bytes)
|
||||
)
|
||||
|
||||
def _txn(txn):
|
||||
for batch in batch_iter(server_name_and_key_ids, 50):
|
||||
_get_keys(txn, batch)
|
||||
return keys
|
||||
|
||||
return self.runInteraction("get_server_verify_keys", _txn)
|
||||
|
||||
def store_server_verify_key(
|
||||
self, server_name, from_server, time_now_ms, verify_key
|
||||
|
@ -140,8 +108,11 @@ class KeyStore(SQLBaseStore):
|
|||
"verify_key": db_binary_type(verify_key.encode()),
|
||||
},
|
||||
)
|
||||
# invalidate takes a tuple corresponding to the params of
|
||||
# _get_server_verify_key. _get_server_verify_key only takes one
|
||||
# param, which is itself the 2-tuple (server_name, key_id).
|
||||
txn.call_after(
|
||||
self._get_server_verify_key.invalidate, (server_name, key_id)
|
||||
self._get_server_verify_key.invalidate, ((server_name, key_id),)
|
||||
)
|
||||
|
||||
return self.runInteraction("store_server_verify_key", _txn)
|
||||
|
@ -188,8 +159,8 @@ class KeyStore(SQLBaseStore):
|
|||
Args:
|
||||
server_keys (list): List of (server_name, key_id, source) triplets.
|
||||
Returns:
|
||||
Dict mapping (server_name, key_id, source) triplets to dicts with
|
||||
"ts_valid_until_ms" and "key_json" keys.
|
||||
Deferred[dict[Tuple[str, str, str|None], list[dict]]]:
|
||||
Dict mapping (server_name, key_id, source) triplets to lists of dicts
|
||||
"""
|
||||
|
||||
def _get_server_keys_json_txn(txn):
|
||||
|
|
|
@ -32,6 +32,7 @@ class RegistrationWorkerStore(SQLBaseStore):
|
|||
super(RegistrationWorkerStore, self).__init__(db_conn, hs)
|
||||
|
||||
self.config = hs.config
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
@cached()
|
||||
def get_user_by_id(self, user_id):
|
||||
|
@ -86,6 +87,162 @@ class RegistrationWorkerStore(SQLBaseStore):
|
|||
"get_user_by_access_token", self._query_for_auth, token
|
||||
)
|
||||
|
||||
@cachedInlineCallbacks()
|
||||
def get_expiration_ts_for_user(self, user_id):
|
||||
"""Get the expiration timestamp for the account bearing a given user ID.
|
||||
|
||||
Args:
|
||||
user_id (str): The ID of the user.
|
||||
Returns:
|
||||
defer.Deferred: None, if the account has no expiration timestamp,
|
||||
otherwise int representation of the timestamp (as a number of
|
||||
milliseconds since epoch).
|
||||
"""
|
||||
res = yield self._simple_select_one_onecol(
|
||||
table="account_validity",
|
||||
keyvalues={"user_id": user_id},
|
||||
retcol="expiration_ts_ms",
|
||||
allow_none=True,
|
||||
desc="get_expiration_ts_for_user",
|
||||
)
|
||||
defer.returnValue(res)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_account_validity_for_user(self, user_id, expiration_ts, email_sent,
|
||||
renewal_token=None):
|
||||
"""Updates the account validity properties of the given account, with the
|
||||
given values.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the account to update properties for.
|
||||
expiration_ts (int): New expiration date, as a timestamp in milliseconds
|
||||
since epoch.
|
||||
email_sent (bool): True means a renewal email has been sent for this
|
||||
account and there's no need to send another one for the current validity
|
||||
period.
|
||||
renewal_token (str): Renewal token the user can use to extend the validity
|
||||
of their account. Defaults to no token.
|
||||
"""
|
||||
def set_account_validity_for_user_txn(txn):
|
||||
self._simple_update_txn(
|
||||
txn=txn,
|
||||
table="account_validity",
|
||||
keyvalues={"user_id": user_id},
|
||||
updatevalues={
|
||||
"expiration_ts_ms": expiration_ts,
|
||||
"email_sent": email_sent,
|
||||
"renewal_token": renewal_token,
|
||||
},
|
||||
)
|
||||
self._invalidate_cache_and_stream(
|
||||
txn, self.get_expiration_ts_for_user, (user_id,),
|
||||
)
|
||||
|
||||
yield self.runInteraction(
|
||||
"set_account_validity_for_user",
|
||||
set_account_validity_for_user_txn,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_renewal_token_for_user(self, user_id, renewal_token):
|
||||
"""Defines a renewal token for a given user.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to set the renewal token for.
|
||||
renewal_token (str): Random unique string that will be used to renew the
|
||||
user's account.
|
||||
|
||||
Raises:
|
||||
StoreError: The provided token is already set for another user.
|
||||
"""
|
||||
yield self._simple_update_one(
|
||||
table="account_validity",
|
||||
keyvalues={"user_id": user_id},
|
||||
updatevalues={"renewal_token": renewal_token},
|
||||
desc="set_renewal_token_for_user",
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_from_renewal_token(self, renewal_token):
|
||||
"""Get a user ID from a renewal token.
|
||||
|
||||
Args:
|
||||
renewal_token (str): The renewal token to perform the lookup with.
|
||||
|
||||
Returns:
|
||||
defer.Deferred[str]: The ID of the user to which the token belongs.
|
||||
"""
|
||||
res = yield self._simple_select_one_onecol(
|
||||
table="account_validity",
|
||||
keyvalues={"renewal_token": renewal_token},
|
||||
retcol="user_id",
|
||||
desc="get_user_from_renewal_token",
|
||||
)
|
||||
|
||||
defer.returnValue(res)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_renewal_token_for_user(self, user_id):
|
||||
"""Get the renewal token associated with a given user ID.
|
||||
|
||||
Args:
|
||||
user_id (str): The user ID to lookup a token for.
|
||||
|
||||
Returns:
|
||||
defer.Deferred[str]: The renewal token associated with this user ID.
|
||||
"""
|
||||
res = yield self._simple_select_one_onecol(
|
||||
table="account_validity",
|
||||
keyvalues={"user_id": user_id},
|
||||
retcol="renewal_token",
|
||||
desc="get_renewal_token_for_user",
|
||||
)
|
||||
|
||||
defer.returnValue(res)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_users_expiring_soon(self):
|
||||
"""Selects users whose account will expire in the [now, now + renew_at] time
|
||||
window (see configuration for account_validity for information on what renew_at
|
||||
refers to).
|
||||
|
||||
Returns:
|
||||
Deferred: Resolves to a list[dict[user_id (str), expiration_ts_ms (int)]]
|
||||
"""
|
||||
def select_users_txn(txn, now_ms, renew_at):
|
||||
sql = (
|
||||
"SELECT user_id, expiration_ts_ms FROM account_validity"
|
||||
" WHERE email_sent = ? AND (expiration_ts_ms - ?) <= ?"
|
||||
)
|
||||
values = [False, now_ms, renew_at]
|
||||
txn.execute(sql, values)
|
||||
return self.cursor_to_dict(txn)
|
||||
|
||||
res = yield self.runInteraction(
|
||||
"get_users_expiring_soon",
|
||||
select_users_txn,
|
||||
self.clock.time_msec(), self.config.account_validity.renew_at,
|
||||
)
|
||||
|
||||
defer.returnValue(res)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_renewal_mail_status(self, user_id, email_sent):
|
||||
"""Sets or unsets the flag that indicates whether a renewal email has been sent
|
||||
to the user (and the user hasn't renewed their account yet).
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to set/unset the flag for.
|
||||
email_sent (bool): Flag which indicates whether a renewal email has been sent
|
||||
to this user.
|
||||
"""
|
||||
yield self._simple_update_one(
|
||||
table="account_validity",
|
||||
keyvalues={"user_id": user_id},
|
||||
updatevalues={"email_sent": email_sent},
|
||||
desc="set_renewal_mail_status",
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_server_admin(self, user):
|
||||
res = yield self._simple_select_one_onecol(
|
||||
|
@ -425,6 +582,8 @@ class RegistrationStore(
|
|||
columns=["creation_ts"],
|
||||
)
|
||||
|
||||
self._account_validity = hs.config.account_validity
|
||||
|
||||
# we no longer use refresh tokens, but it's possible that some people
|
||||
# might have a background update queued to build this index. Just
|
||||
# clear the background update.
|
||||
|
@ -561,9 +720,23 @@ class RegistrationStore(
|
|||
"user_type": user_type,
|
||||
},
|
||||
)
|
||||
|
||||
except self.database_engine.module.IntegrityError:
|
||||
raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE)
|
||||
|
||||
if self._account_validity.enabled:
|
||||
now_ms = self.clock.time_msec()
|
||||
expiration_ts = now_ms + self._account_validity.period
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"account_validity",
|
||||
values={
|
||||
"user_id": user_id,
|
||||
"expiration_ts_ms": expiration_ts,
|
||||
"email_sent": False,
|
||||
}
|
||||
)
|
||||
|
||||
if token:
|
||||
# it's possible for this to get a conflict, but only for a single user
|
||||
# since tokens are namespaced based on their user ID
|
||||
|
|
|
@ -13,19 +13,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS application_services(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
url TEXT,
|
||||
token TEXT,
|
||||
hs_token TEXT,
|
||||
sender TEXT,
|
||||
UNIQUE(token)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS application_services_regex(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
as_id BIGINT UNSIGNED NOT NULL,
|
||||
namespace INTEGER, /* enum[room_id|room_alias|user_id] */
|
||||
regex TEXT,
|
||||
FOREIGN KEY(as_id) REFERENCES application_services(id)
|
||||
);
|
||||
/* We used to create a tables called application_services and
|
||||
* application_services_regex, but these are no longer used and are removed in
|
||||
* delta 54.
|
||||
*/
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
# Copyright 2015, 2016 OpenMarket 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
|
||||
|
||||
import simplejson as json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_create(cur, *args, **kwargs):
|
||||
cur.execute("SELECT id, regex FROM application_services_regex")
|
||||
for row in cur.fetchall():
|
||||
try:
|
||||
logger.debug("Checking %s..." % row[0])
|
||||
json.loads(row[1])
|
||||
except ValueError:
|
||||
# row isn't in json, make it so.
|
||||
string_regex = row[1]
|
||||
new_regex = json.dumps({
|
||||
"regex": string_regex,
|
||||
"exclusive": True
|
||||
})
|
||||
cur.execute(
|
||||
"UPDATE application_services_regex SET regex=? WHERE id=?",
|
||||
(new_regex, row[0])
|
||||
)
|
||||
|
||||
|
||||
def run_upgrade(*args, **kwargs):
|
||||
pass
|
|
@ -17,14 +17,6 @@ DELETE FROM room_memberships WHERE rowid not in (
|
|||
DROP INDEX IF EXISTS room_memberships_event_id;
|
||||
CREATE UNIQUE INDEX room_memberships_event_id ON room_memberships(event_id);
|
||||
|
||||
--
|
||||
DELETE FROM feedback WHERE rowid not in (
|
||||
SELECT MIN(rowid) FROM feedback GROUP BY event_id
|
||||
);
|
||||
|
||||
DROP INDEX IF EXISTS feedback_event_id;
|
||||
CREATE UNIQUE INDEX feedback_event_id ON feedback(event_id);
|
||||
|
||||
--
|
||||
DELETE FROM topics WHERE rowid not in (
|
||||
SELECT MIN(rowid) FROM topics GROUP BY event_id
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2015, 2016 OpenMarket Ltd
|
||||
/* 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.
|
||||
|
@ -13,10 +13,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
-- Should only ever contain one row
|
||||
CREATE TABLE IF NOT EXISTS stats_reporting(
|
||||
-- The stream ordering token which was most recently reported as stats
|
||||
reported_stream_token INTEGER,
|
||||
-- The time (seconds since epoch) stats were most recently reported
|
||||
reported_time BIGINT
|
||||
);
|
||||
/* We used to create a table called stats_reporting, but this is no longer
|
||||
* used and is removed in delta 54.
|
||||
*/
|
|
@ -14,15 +14,10 @@
|
|||
*/
|
||||
|
||||
|
||||
/**
|
||||
* The positions in the event stream_ordering when the current_state was
|
||||
* replaced by the state at the event.
|
||||
/* We used to create a table called current_state_resets, but this is no
|
||||
* longer used and is removed in delta 54.
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS current_state_resets(
|
||||
event_stream_ordering BIGINT PRIMARY KEY NOT NULL
|
||||
);
|
||||
|
||||
/* The outlier events that have aquired a state group typically through
|
||||
* backfill. This is tracked separately to the events table, as assigning a
|
||||
* state group change the position of the existing event in the stream
|
||||
|
|
|
@ -24,13 +24,9 @@ DROP INDEX IF EXISTS state_groups_id; -- Duplicate of PRIMARY KEY
|
|||
DROP INDEX IF EXISTS event_to_state_groups_id; -- Duplicate of PRIMARY KEY
|
||||
DROP INDEX IF EXISTS event_push_actions_room_id_event_id_user_id_profile_tag; -- Duplicate of UNIQUE CONSTRAINT
|
||||
|
||||
DROP INDEX IF EXISTS event_destinations_id; -- Prefix of UNIQUE CONSTRAINT
|
||||
DROP INDEX IF EXISTS st_extrem_id; -- Prefix of UNIQUE CONSTRAINT
|
||||
DROP INDEX IF EXISTS event_content_hashes_id; -- Prefix of UNIQUE CONSTRAINT
|
||||
DROP INDEX IF EXISTS event_signatures_id; -- Prefix of UNIQUE CONSTRAINT
|
||||
DROP INDEX IF EXISTS event_edge_hashes_id; -- Prefix of UNIQUE CONSTRAINT
|
||||
DROP INDEX IF EXISTS redactions_event_id; -- Duplicate of UNIQUE CONSTRAINT
|
||||
DROP INDEX IF EXISTS room_hosts_room_id; -- Prefix of UNIQUE CONSTRAINT
|
||||
|
||||
-- The following indices were unused
|
||||
DROP INDEX IF EXISTS remote_media_cache_thumbnails_media_id;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2014-2016 OpenMarket Ltd
|
||||
/* 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.
|
||||
|
@ -13,12 +13,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_aliases(
|
||||
room_alias TEXT NOT NULL,
|
||||
room_id TEXT NOT NULL
|
||||
DROP TABLE IF EXISTS account_validity;
|
||||
|
||||
-- Track what users are in public rooms.
|
||||
CREATE TABLE IF NOT EXISTS account_validity (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
expiration_ts_ms BIGINT NOT NULL,
|
||||
email_sent BOOLEAN NOT NULL,
|
||||
renewal_token TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_alias_servers(
|
||||
room_alias TEXT NOT NULL,
|
||||
server TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX account_validity_email_sent_idx ON account_validity(email_sent, expiration_ts_ms)
|
||||
CREATE UNIQUE INDEX account_validity_renewal_string_idx ON account_validity(renewal_token)
|
30
synapse/storage/schema/delta/54/drop_legacy_tables.sql
Normal file
30
synapse/storage/schema/delta/54/drop_legacy_tables.sql
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
-- we need to do this first due to foreign constraints
|
||||
DROP TABLE IF EXISTS application_services_regex;
|
||||
|
||||
DROP TABLE IF EXISTS application_services;
|
||||
DROP TABLE IF EXISTS transaction_id_to_pdu;
|
||||
DROP TABLE IF EXISTS stats_reporting;
|
||||
DROP TABLE IF EXISTS current_state_resets;
|
||||
DROP TABLE IF EXISTS event_content_hashes;
|
||||
DROP TABLE IF EXISTS event_destinations;
|
||||
DROP TABLE IF EXISTS event_edge_hashes;
|
||||
DROP TABLE IF EXISTS event_signatures;
|
||||
DROP TABLE IF EXISTS feedback;
|
||||
DROP TABLE IF EXISTS room_hosts;
|
||||
DROP TABLE IF EXISTS server_tls_certificates;
|
||||
DROP TABLE IF EXISTS state_forward_extremities;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue