mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Merge branch 'develop' into jaywink/admin-forward-extremities
This commit is contained in:
commit
8965b6cfec
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
|||||||
_trial_temp/
|
_trial_temp/
|
||||||
_trial_temp*/
|
_trial_temp*/
|
||||||
/out
|
/out
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# stuff that is likely to exist when you run a server locally
|
# stuff that is likely to exist when you run a server locally
|
||||||
/*.db
|
/*.db
|
||||||
|
125
CHANGES.md
125
CHANGES.md
@ -1,6 +1,118 @@
|
|||||||
Synapse 1.25.0rc1 (2021-01-06)
|
Synapse 1.26.0rc1 (2021-01-20)
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
This release brings a new schema version for Synapse and rolling back to a previous
|
||||||
|
version is not trivial. Please review [UPGRADE.rst](UPGRADE.rst) for more details
|
||||||
|
on these changes and for general upgrade guidance.
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- Add support for multiple SSO Identity Providers. ([\#9015](https://github.com/matrix-org/synapse/issues/9015), [\#9017](https://github.com/matrix-org/synapse/issues/9017), [\#9036](https://github.com/matrix-org/synapse/issues/9036), [\#9067](https://github.com/matrix-org/synapse/issues/9067), [\#9081](https://github.com/matrix-org/synapse/issues/9081), [\#9082](https://github.com/matrix-org/synapse/issues/9082), [\#9105](https://github.com/matrix-org/synapse/issues/9105), [\#9107](https://github.com/matrix-org/synapse/issues/9107), [\#9109](https://github.com/matrix-org/synapse/issues/9109), [\#9110](https://github.com/matrix-org/synapse/issues/9110), [\#9127](https://github.com/matrix-org/synapse/issues/9127), [\#9153](https://github.com/matrix-org/synapse/issues/9153), [\#9154](https://github.com/matrix-org/synapse/issues/9154), [\#9177](https://github.com/matrix-org/synapse/issues/9177))
|
||||||
|
- During user-interactive authentication via single-sign-on, give a better error if the user uses the wrong account on the SSO IdP. ([\#9091](https://github.com/matrix-org/synapse/issues/9091))
|
||||||
|
- Give the `public_baseurl` a default value, if it is not explicitly set in the configuration file. ([\#9159](https://github.com/matrix-org/synapse/issues/9159))
|
||||||
|
- Improve performance when calculating ignored users in large rooms. ([\#9024](https://github.com/matrix-org/synapse/issues/9024))
|
||||||
|
- Implement [MSC2176](https://github.com/matrix-org/matrix-doc/pull/2176) in an experimental room version. ([\#8984](https://github.com/matrix-org/synapse/issues/8984))
|
||||||
|
- Add an admin API for protecting local media from quarantine. ([\#9086](https://github.com/matrix-org/synapse/issues/9086))
|
||||||
|
- Remove a user's avatar URL and display name when deactivated with the Admin API. ([\#8932](https://github.com/matrix-org/synapse/issues/8932))
|
||||||
|
- Update `/_synapse/admin/v1/users/<user_id>/joined_rooms` to work for both local and remote users. ([\#8948](https://github.com/matrix-org/synapse/issues/8948))
|
||||||
|
- Add experimental support for handling to-device messages on worker processes. ([\#9042](https://github.com/matrix-org/synapse/issues/9042), [\#9043](https://github.com/matrix-org/synapse/issues/9043), [\#9044](https://github.com/matrix-org/synapse/issues/9044), [\#9130](https://github.com/matrix-org/synapse/issues/9130))
|
||||||
|
- Add experimental support for handling `/keys/claim` and `/room_keys` APIs on worker processes. ([\#9068](https://github.com/matrix-org/synapse/issues/9068))
|
||||||
|
- Add experimental support for handling `/devices` API on worker processes. ([\#9092](https://github.com/matrix-org/synapse/issues/9092))
|
||||||
|
- Add experimental support for moving off receipts and account data persistence off master. ([\#9104](https://github.com/matrix-org/synapse/issues/9104), [\#9166](https://github.com/matrix-org/synapse/issues/9166))
|
||||||
|
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- Fix a long-standing issue where an internal server error would occur when requesting a profile over federation that did not include a display name / avatar URL. ([\#9023](https://github.com/matrix-org/synapse/issues/9023))
|
||||||
|
- Fix a long-standing bug where some caches could grow larger than configured. ([\#9028](https://github.com/matrix-org/synapse/issues/9028))
|
||||||
|
- Fix error handling during insertion of client IPs into the database. ([\#9051](https://github.com/matrix-org/synapse/issues/9051))
|
||||||
|
- Fix bug where we didn't correctly record CPU time spent in `on_new_event` block. ([\#9053](https://github.com/matrix-org/synapse/issues/9053))
|
||||||
|
- Fix a minor bug which could cause confusing error messages from invalid configurations. ([\#9054](https://github.com/matrix-org/synapse/issues/9054))
|
||||||
|
- Fix incorrect exit code when there is an error at startup. ([\#9059](https://github.com/matrix-org/synapse/issues/9059))
|
||||||
|
- Fix `JSONDecodeError` spamming the logs when sending transactions to remote servers. ([\#9070](https://github.com/matrix-org/synapse/issues/9070))
|
||||||
|
- Fix "Failed to send request" errors when a client provides an invalid room alias. ([\#9071](https://github.com/matrix-org/synapse/issues/9071))
|
||||||
|
- Fix bugs in federation catchup logic that caused outbound federation to be delayed for large servers after start up. Introduced in v1.8.0 and v1.21.0. ([\#9114](https://github.com/matrix-org/synapse/issues/9114), [\#9116](https://github.com/matrix-org/synapse/issues/9116))
|
||||||
|
- Fix corruption of `pushers` data when a postgres bouncer is used. ([\#9117](https://github.com/matrix-org/synapse/issues/9117))
|
||||||
|
- Fix minor bugs in handling the `clientRedirectUrl` parameter for SSO login. ([\#9128](https://github.com/matrix-org/synapse/issues/9128))
|
||||||
|
- Fix "Unhandled error in Deferred: BodyExceededMaxSize" errors when .well-known files that are too large. ([\#9108](https://github.com/matrix-org/synapse/issues/9108))
|
||||||
|
- Fix "UnboundLocalError: local variable 'length' referenced before assignment" errors when the response body exceeds the expected size. This bug was introduced in v1.25.0. ([\#9145](https://github.com/matrix-org/synapse/issues/9145))
|
||||||
|
- Fix a long-standing bug "ValueError: invalid literal for int() with base 10" when `/publicRooms` is requested with an invalid `server` parameter. ([\#9161](https://github.com/matrix-org/synapse/issues/9161))
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- Add some extra docs for getting Synapse running on macOS. ([\#8997](https://github.com/matrix-org/synapse/issues/8997))
|
||||||
|
- Correct a typo in the `systemd-with-workers` documentation. ([\#9035](https://github.com/matrix-org/synapse/issues/9035))
|
||||||
|
- Correct a typo in `INSTALL.md`. ([\#9040](https://github.com/matrix-org/synapse/issues/9040))
|
||||||
|
- Add missing `user_mapping_provider` configuration to the Keycloak OIDC example. Contributed by @chris-ruecker. ([\#9057](https://github.com/matrix-org/synapse/issues/9057))
|
||||||
|
- Quote `pip install` packages when extras are used to avoid shells interpreting bracket characters. ([\#9151](https://github.com/matrix-org/synapse/issues/9151))
|
||||||
|
|
||||||
|
|
||||||
|
Deprecations and Removals
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
- Remove broken and unmaintained `demo/webserver.py` script. ([\#9039](https://github.com/matrix-org/synapse/issues/9039))
|
||||||
|
|
||||||
|
|
||||||
|
Internal Changes
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- Improve efficiency of large state resolutions. ([\#8868](https://github.com/matrix-org/synapse/issues/8868), [\#9029](https://github.com/matrix-org/synapse/issues/9029), [\#9115](https://github.com/matrix-org/synapse/issues/9115), [\#9118](https://github.com/matrix-org/synapse/issues/9118), [\#9124](https://github.com/matrix-org/synapse/issues/9124))
|
||||||
|
- Various clean-ups to the structured logging and logging context code. ([\#8939](https://github.com/matrix-org/synapse/issues/8939))
|
||||||
|
- Ensure rejected events get added to some metadata tables. ([\#9016](https://github.com/matrix-org/synapse/issues/9016))
|
||||||
|
- Ignore date-rotated homeserver logs saved to disk. ([\#9018](https://github.com/matrix-org/synapse/issues/9018))
|
||||||
|
- Remove an unused column from `access_tokens` table. ([\#9025](https://github.com/matrix-org/synapse/issues/9025))
|
||||||
|
- Add a `-noextras` factor to `tox.ini`, to support running the tests with no optional dependencies. ([\#9030](https://github.com/matrix-org/synapse/issues/9030))
|
||||||
|
- Fix running unit tests when optional dependencies are not installed. ([\#9031](https://github.com/matrix-org/synapse/issues/9031))
|
||||||
|
- Allow bumping schema version when using split out state database. ([\#9033](https://github.com/matrix-org/synapse/issues/9033))
|
||||||
|
- Configure the linters to run on a consistent set of files. ([\#9038](https://github.com/matrix-org/synapse/issues/9038))
|
||||||
|
- Various cleanups to device inbox store. ([\#9041](https://github.com/matrix-org/synapse/issues/9041))
|
||||||
|
- Drop unused database tables. ([\#9055](https://github.com/matrix-org/synapse/issues/9055))
|
||||||
|
- Remove unused `SynapseService` class. ([\#9058](https://github.com/matrix-org/synapse/issues/9058))
|
||||||
|
- Remove unnecessary declarations in the tests for the admin API. ([\#9063](https://github.com/matrix-org/synapse/issues/9063))
|
||||||
|
- Remove `SynapseRequest.get_user_agent`. ([\#9069](https://github.com/matrix-org/synapse/issues/9069))
|
||||||
|
- Remove redundant `Homeserver.get_ip_from_request` method. ([\#9080](https://github.com/matrix-org/synapse/issues/9080))
|
||||||
|
- Add type hints to media repository. ([\#9093](https://github.com/matrix-org/synapse/issues/9093))
|
||||||
|
- Fix the wrong arguments being passed to `BlacklistingAgentWrapper` from `MatrixFederationAgent`. Contributed by Timothy Leung. ([\#9098](https://github.com/matrix-org/synapse/issues/9098))
|
||||||
|
- Reduce the scope of caught exceptions in `BlacklistingAgentWrapper`. ([\#9106](https://github.com/matrix-org/synapse/issues/9106))
|
||||||
|
- Improve `UsernamePickerTestCase`. ([\#9112](https://github.com/matrix-org/synapse/issues/9112))
|
||||||
|
- Remove dependency on `distutils`. ([\#9125](https://github.com/matrix-org/synapse/issues/9125))
|
||||||
|
- Enforce that replication HTTP clients are called with keyword arguments only. ([\#9144](https://github.com/matrix-org/synapse/issues/9144))
|
||||||
|
- Fix the Python 3.5 / old dependencies build in CI. ([\#9146](https://github.com/matrix-org/synapse/issues/9146))
|
||||||
|
- Replace the old `perspectives` option in the Synapse docker config file template with `trusted_key_servers`. ([\#9157](https://github.com/matrix-org/synapse/issues/9157))
|
||||||
|
|
||||||
|
|
||||||
|
Synapse 1.25.0 (2021-01-13)
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Ending Support for Python 3.5 and Postgres 9.5
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
With this release, the Synapse team is announcing a formal deprecation policy for our platform dependencies, like Python and PostgreSQL:
|
||||||
|
|
||||||
|
All future releases of Synapse will follow the upstream end-of-life schedules.
|
||||||
|
|
||||||
|
Which means:
|
||||||
|
|
||||||
|
* This is the last release which guarantees support for Python 3.5.
|
||||||
|
* We will end support for PostgreSQL 9.5 early next month.
|
||||||
|
* We will end support for Python 3.6 and PostgreSQL 9.6 near the end of the year.
|
||||||
|
|
||||||
|
Crucially, this means __we will not produce .deb packages for Debian 9 (Stretch) or Ubuntu 16.04 (Xenial)__ beyond the transition period described below.
|
||||||
|
|
||||||
|
The website https://endoflife.date/ has convenient summaries of the support schedules for projects like [Python](https://endoflife.date/python) and [PostgreSQL](https://endoflife.date/postgresql).
|
||||||
|
|
||||||
|
If you are unable to upgrade your environment to a supported version of Python or Postgres, we encourage you to consider using the [Synapse Docker images](./INSTALL.md#docker-images-and-ansible-playbooks) instead.
|
||||||
|
|
||||||
|
### Transition Period
|
||||||
|
|
||||||
|
We will make a good faith attempt to avoid breaking compatibility in all releases through the end of March 2021. However, critical security vulnerabilities in dependencies or other unanticipated circumstances may arise which necessitate breaking compatibility earlier.
|
||||||
|
|
||||||
|
We intend to continue producing .deb packages for Debian 9 (Stretch) and Ubuntu 16.04 (Xenial) through the transition period.
|
||||||
|
|
||||||
Removal warning
|
Removal warning
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
@ -12,6 +124,15 @@ are deprecated and will be removed in a future release. They will be replaced by
|
|||||||
`POST /_synapse/admin/v1/rooms/<room_id>/delete` replaces `POST /_synapse/admin/v1/purge_room` and
|
`POST /_synapse/admin/v1/rooms/<room_id>/delete` replaces `POST /_synapse/admin/v1/purge_room` and
|
||||||
`POST /_synapse/admin/v1/shutdown_room/<room_id>`.
|
`POST /_synapse/admin/v1/shutdown_room/<room_id>`.
|
||||||
|
|
||||||
|
Bugfixes
|
||||||
|
--------
|
||||||
|
|
||||||
|
- Fix HTTP proxy support when using a proxy that is on a blacklisted IP. Introduced in v1.25.0rc1. Contributed by @Bubu. ([\#9084](https://github.com/matrix-org/synapse/issues/9084))
|
||||||
|
|
||||||
|
|
||||||
|
Synapse 1.25.0rc1 (2021-01-06)
|
||||||
|
==============================
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
@ -61,7 +182,7 @@ Improved Documentation
|
|||||||
- Combine related media admin API docs. ([\#8839](https://github.com/matrix-org/synapse/issues/8839))
|
- Combine related media admin API docs. ([\#8839](https://github.com/matrix-org/synapse/issues/8839))
|
||||||
- Fix an error in the documentation for the SAML username mapping provider. ([\#8873](https://github.com/matrix-org/synapse/issues/8873))
|
- Fix an error in the documentation for the SAML username mapping provider. ([\#8873](https://github.com/matrix-org/synapse/issues/8873))
|
||||||
- Clarify comments around template directories in `sample_config.yaml`. ([\#8891](https://github.com/matrix-org/synapse/issues/8891))
|
- Clarify comments around template directories in `sample_config.yaml`. ([\#8891](https://github.com/matrix-org/synapse/issues/8891))
|
||||||
- Moved instructions for database setup, adjusted heading levels and improved syntax highlighting in [INSTALL.md](../INSTALL.md). Contributed by fossterer. ([\#8987](https://github.com/matrix-org/synapse/issues/8987))
|
- Move instructions for database setup, adjusted heading levels and improved syntax highlighting in [INSTALL.md](../INSTALL.md). Contributed by @fossterer. ([\#8987](https://github.com/matrix-org/synapse/issues/8987))
|
||||||
- Update the example value of `group_creation_prefix` in the sample configuration. ([\#8992](https://github.com/matrix-org/synapse/issues/8992))
|
- Update the example value of `group_creation_prefix` in the sample configuration. ([\#8992](https://github.com/matrix-org/synapse/issues/8992))
|
||||||
- Link the Synapse developer room to the development section in the docs. ([\#9002](https://github.com/matrix-org/synapse/issues/9002))
|
- Link the Synapse developer room to the development section in the docs. ([\#9002](https://github.com/matrix-org/synapse/issues/9002))
|
||||||
|
|
||||||
|
@ -190,7 +190,8 @@ via brew and inform `pip` about it so that `psycopg2` builds:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
brew install openssl@1.1
|
brew install openssl@1.1
|
||||||
export LDFLAGS=-L/usr/local/Cellar/openssl\@1.1/1.1.1d/lib/
|
export LDFLAGS="-L/usr/local/opt/openssl/lib"
|
||||||
|
export CPPFLAGS="-I/usr/local/opt/openssl/include"
|
||||||
```
|
```
|
||||||
|
|
||||||
##### OpenSUSE
|
##### OpenSUSE
|
||||||
@ -257,7 +258,7 @@ for a number of platforms.
|
|||||||
|
|
||||||
#### Docker images and Ansible playbooks
|
#### Docker images and Ansible playbooks
|
||||||
|
|
||||||
There is an offical synapse image available at
|
There is an official synapse image available at
|
||||||
<https://hub.docker.com/r/matrixdotorg/synapse> which can be used with
|
<https://hub.docker.com/r/matrixdotorg/synapse> which can be used with
|
||||||
the docker-compose file available at [contrib/docker](contrib/docker). Further
|
the docker-compose file available at [contrib/docker](contrib/docker). Further
|
||||||
information on this including configuration options is available in the README
|
information on this including configuration options is available in the README
|
||||||
|
23
README.rst
23
README.rst
@ -243,7 +243,7 @@ Then update the ``users`` table in the database::
|
|||||||
Synapse Development
|
Synapse Development
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Join our developer community on Matrix: [#synapse-dev:matrix.org](https://matrix.to/#/#synapse-dev:matrix.org)
|
Join our developer community on Matrix: `#synapse-dev:matrix.org <https://matrix.to/#/#synapse-dev:matrix.org>`_
|
||||||
|
|
||||||
Before setting up a development environment for synapse, make sure you have the
|
Before setting up a development environment for synapse, make sure you have the
|
||||||
system dependencies (such as the python header files) installed - see
|
system dependencies (such as the python header files) installed - see
|
||||||
@ -280,6 +280,27 @@ differ)::
|
|||||||
|
|
||||||
PASSED (skips=15, successes=1322)
|
PASSED (skips=15, successes=1322)
|
||||||
|
|
||||||
|
We recommend using the demo which starts 3 federated instances running on ports `8080` - `8082`
|
||||||
|
|
||||||
|
./demo/start.sh
|
||||||
|
|
||||||
|
(to stop, you can use `./demo/stop.sh`)
|
||||||
|
|
||||||
|
If you just want to start a single instance of the app and run it directly::
|
||||||
|
|
||||||
|
# Create the homeserver.yaml config once
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
|
--server-name my.domain.name \
|
||||||
|
--config-path homeserver.yaml \
|
||||||
|
--generate-config \
|
||||||
|
--report-stats=[yes|no]
|
||||||
|
|
||||||
|
# Start the app
|
||||||
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Running the Integration Tests
|
Running the Integration Tests
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
72
UPGRADE.rst
72
UPGRADE.rst
@ -5,6 +5,16 @@ Before upgrading check if any special steps are required to upgrade from the
|
|||||||
version you currently have installed to the current version of Synapse. The extra
|
version you currently have installed to the current version of Synapse. The extra
|
||||||
instructions that may be required are listed later in this document.
|
instructions that may be required are listed later in this document.
|
||||||
|
|
||||||
|
* Check that your versions of Python and PostgreSQL are still supported.
|
||||||
|
|
||||||
|
Synapse follows upstream lifecycles for `Python`_ and `PostgreSQL`_, and
|
||||||
|
removes support for versions which are no longer maintained.
|
||||||
|
|
||||||
|
The website https://endoflife.date also offers convenient summaries.
|
||||||
|
|
||||||
|
.. _Python: https://devguide.python.org/devcycle/#end-of-life-branches
|
||||||
|
.. _PostgreSQL: https://www.postgresql.org/support/versioning/
|
||||||
|
|
||||||
* If Synapse was installed using `prebuilt packages
|
* If Synapse was installed using `prebuilt packages
|
||||||
<INSTALL.md#prebuilt-packages>`_, you will need to follow the normal process
|
<INSTALL.md#prebuilt-packages>`_, you will need to follow the normal process
|
||||||
for upgrading those packages.
|
for upgrading those packages.
|
||||||
@ -75,9 +85,71 @@ for example:
|
|||||||
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||||
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||||
|
|
||||||
|
Upgrading to v1.26.0
|
||||||
|
====================
|
||||||
|
|
||||||
|
Rolling back to v1.25.0 after a failed upgrade
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
v1.26.0 includes a lot of large changes. If something problematic occurs, you
|
||||||
|
may want to roll-back to a previous version of Synapse. Because v1.26.0 also
|
||||||
|
includes a new database schema version, reverting that version is also required
|
||||||
|
alongside the generic rollback instructions mentioned above. In short, to roll
|
||||||
|
back to v1.25.0 you need to:
|
||||||
|
|
||||||
|
1. Stop the server
|
||||||
|
2. Decrease the schema version in the database:
|
||||||
|
|
||||||
|
.. code:: sql
|
||||||
|
|
||||||
|
UPDATE schema_version SET version = 58;
|
||||||
|
|
||||||
|
3. Delete the ignored users & chain cover data:
|
||||||
|
|
||||||
|
.. code:: sql
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS ignored_users;
|
||||||
|
UPDATE rooms SET has_auth_chain_index = false;
|
||||||
|
|
||||||
|
For PostgreSQL run:
|
||||||
|
|
||||||
|
.. code:: sql
|
||||||
|
|
||||||
|
TRUNCATE event_auth_chain_links;
|
||||||
|
TRUNCATE event_auth_chains;
|
||||||
|
|
||||||
|
For SQLite run:
|
||||||
|
|
||||||
|
.. code:: sql
|
||||||
|
|
||||||
|
DELETE FROM event_auth_chain_links;
|
||||||
|
DELETE FROM event_auth_chains;
|
||||||
|
|
||||||
|
4. Mark the deltas as not run (so they will re-run on upgrade).
|
||||||
|
|
||||||
|
.. code:: sql
|
||||||
|
|
||||||
|
DELETE FROM applied_schema_deltas WHERE version = 59 AND file = "59/01ignored_user.py";
|
||||||
|
DELETE FROM applied_schema_deltas WHERE version = 59 AND file = "59/06chain_cover_index.sql";
|
||||||
|
|
||||||
|
5. Downgrade Synapse by following the instructions for your installation method
|
||||||
|
in the "Rolling back to older versions" section above.
|
||||||
|
|
||||||
Upgrading to v1.25.0
|
Upgrading to v1.25.0
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
Last release supporting Python 3.5
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
This is the last release of Synapse which guarantees support with Python 3.5,
|
||||||
|
which passed its upstream End of Life date several months ago.
|
||||||
|
|
||||||
|
We will attempt to maintain support through March 2021, but without guarantees.
|
||||||
|
|
||||||
|
In the future, Synapse will follow upstream schedules for ending support of
|
||||||
|
older versions of Python and PostgreSQL. Please upgrade to at least Python 3.6
|
||||||
|
and PostgreSQL 9.6 as soon as possible.
|
||||||
|
|
||||||
Blacklisting IP ranges
|
Blacklisting IP ranges
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
Various clean-ups to the structured logging and logging context code.
|
|
@ -1 +0,0 @@
|
|||||||
Implement [MSC2176](https://github.com/matrix-org/matrix-doc/pull/2176) in an experimental room version.
|
|
@ -1 +0,0 @@
|
|||||||
Add support for multiple SSO Identity Providers.
|
|
@ -1 +0,0 @@
|
|||||||
Add support for multiple SSO Identity Providers.
|
|
@ -1 +0,0 @@
|
|||||||
Ignore date-rotated homeserver logs saved to disk.
|
|
@ -1 +0,0 @@
|
|||||||
Fix a longstanding issue where an internal server error would occur when requesting a profile over federation that did not include a display name / avatar URL.
|
|
@ -1 +0,0 @@
|
|||||||
Improved performance when calculating ignored users in large rooms.
|
|
@ -1 +0,0 @@
|
|||||||
Fix a long-standing bug where some caches could grow larger than configured.
|
|
@ -1 +0,0 @@
|
|||||||
Add a `-noextras` factor to `tox.ini`, to support running the tests with no optional dependencies.
|
|
@ -1 +0,0 @@
|
|||||||
Fix running unit tests when optional dependencies are not installed.
|
|
@ -1 +0,0 @@
|
|||||||
Allow bumping schema version when using split out state database.
|
|
@ -1 +0,0 @@
|
|||||||
Corrected a typo in the `systemd-with-workers` documentation.
|
|
@ -1 +0,0 @@
|
|||||||
Add support for multiple SSO Identity Providers.
|
|
@ -1 +0,0 @@
|
|||||||
Configure the linters to run on a consistent set of files.
|
|
@ -1 +0,0 @@
|
|||||||
Remove broken and unmaintained `demo/webserver.py` script.
|
|
@ -1 +0,0 @@
|
|||||||
Various cleanups to device inbox store.
|
|
@ -1 +0,0 @@
|
|||||||
Add experimental support for handling and persistence of to-device messages to happen on worker processes.
|
|
@ -1 +0,0 @@
|
|||||||
Add experimental support for handling and persistence of to-device messages to happen on worker processes.
|
|
@ -1 +0,0 @@
|
|||||||
Add experimental support for handling and persistence of to-device messages to happen on worker processes.
|
|
1
changelog.d/9045.misc
Normal file
1
changelog.d/9045.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add tests to `test_user.UsersListTestCase` for List Users Admin API.
|
@ -1 +0,0 @@
|
|||||||
Fix error handling during insertion of client IPs into the database.
|
|
@ -1 +0,0 @@
|
|||||||
Fix bug where we didn't correctly record CPU time spent in 'on_new_event' block.
|
|
@ -1 +0,0 @@
|
|||||||
Fix a minor bug which could cause confusing error messages from invalid configurations.
|
|
@ -1 +0,0 @@
|
|||||||
Add missing user_mapping_provider configuration to the Keycloak OIDC example. Contributed by @chris-ruecker.
|
|
1
changelog.d/9129.misc
Normal file
1
changelog.d/9129.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Various improvements to the federation client.
|
1
changelog.d/9135.doc
Normal file
1
changelog.d/9135.doc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add link to Matrix VoIP tester for turn-howto.
|
1
changelog.d/9163.bugfix
Normal file
1
changelog.d/9163.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix a long-standing bug where Synapse would return a 500 error when a thumbnail did not exist (and auto-generation of thumbnails was not enabled).
|
1
changelog.d/9176.misc
Normal file
1
changelog.d/9176.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Speed up chain cover calculation when persisting a batch of state events at once.
|
1
changelog.d/9180.misc
Normal file
1
changelog.d/9180.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add a `long_description_type` to the package metadata.
|
1
changelog.d/9181.misc
Normal file
1
changelog.d/9181.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Speed up batch insertion when using PostgreSQL.
|
1
changelog.d/9184.misc
Normal file
1
changelog.d/9184.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Emit an error at startup if different Identity Providers are configured with the same `idp_id`.
|
1
changelog.d/9188.misc
Normal file
1
changelog.d/9188.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Speed up batch insertion when using PostgreSQL.
|
1
changelog.d/9189.misc
Normal file
1
changelog.d/9189.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add an `oidc-` prefix to any `idp_id`s which are given in the `oidc_providers` configuration.
|
1
changelog.d/9190.misc
Normal file
1
changelog.d/9190.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Improve performance of concurrent use of `StreamIDGenerators`.
|
1
changelog.d/9191.misc
Normal file
1
changelog.d/9191.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add some missing source directories to the automatic linting script.
|
1
changelog.d/9193.bugfix
Normal file
1
changelog.d/9193.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix receipts or account data not being sent down sync. Introduced in v1.26.0rc1.
|
1
changelog.d/9195.bugfix
Normal file
1
changelog.d/9195.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix receipts or account data not being sent down sync. Introduced in v1.26.0rc1.
|
17
debian/changelog
vendored
17
debian/changelog
vendored
@ -1,3 +1,20 @@
|
|||||||
|
matrix-synapse-py3 (1.25.0ubuntu1) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
|
* Remove dependency on `python3-distutils`.
|
||||||
|
|
||||||
|
-- Richard van der Hoff <richard@matrix.org> Fri, 15 Jan 2021 12:44:19 +0000
|
||||||
|
|
||||||
|
matrix-synapse-py3 (1.25.0) stable; urgency=medium
|
||||||
|
|
||||||
|
[ Dan Callahan ]
|
||||||
|
* Update dependencies to account for the removal of the transitional
|
||||||
|
dh-systemd package from Debian Bullseye.
|
||||||
|
|
||||||
|
[ Synapse Packaging team ]
|
||||||
|
* New synapse release 1.25.0.
|
||||||
|
|
||||||
|
-- Synapse Packaging team <packages@matrix.org> Wed, 13 Jan 2021 10:14:55 +0000
|
||||||
|
|
||||||
matrix-synapse-py3 (1.24.0) stable; urgency=medium
|
matrix-synapse-py3 (1.24.0) stable; urgency=medium
|
||||||
|
|
||||||
* New synapse release 1.24.0.
|
* New synapse release 1.24.0.
|
||||||
|
7
debian/control
vendored
7
debian/control
vendored
@ -3,9 +3,11 @@ Section: contrib/python
|
|||||||
Priority: extra
|
Priority: extra
|
||||||
Maintainer: Synapse Packaging team <packages@matrix.org>
|
Maintainer: Synapse Packaging team <packages@matrix.org>
|
||||||
# keep this list in sync with the build dependencies in docker/Dockerfile-dhvirtualenv.
|
# keep this list in sync with the build dependencies in docker/Dockerfile-dhvirtualenv.
|
||||||
|
# TODO: Remove the dependency on dh-systemd after dropping support for Ubuntu xenial
|
||||||
|
# On all other supported releases, it's merely a transitional package which
|
||||||
|
# does nothing but depends on debhelper (> 9.20160709)
|
||||||
Build-Depends:
|
Build-Depends:
|
||||||
debhelper (>= 9),
|
debhelper (>= 9.20160709) | dh-systemd,
|
||||||
dh-systemd,
|
|
||||||
dh-virtualenv (>= 1.1),
|
dh-virtualenv (>= 1.1),
|
||||||
libsystemd-dev,
|
libsystemd-dev,
|
||||||
libpq-dev,
|
libpq-dev,
|
||||||
@ -29,7 +31,6 @@ Pre-Depends: dpkg (>= 1.16.1)
|
|||||||
Depends:
|
Depends:
|
||||||
adduser,
|
adduser,
|
||||||
debconf,
|
debconf,
|
||||||
python3-distutils|libpython3-stdlib (<< 3.6),
|
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
${shlibs:Depends},
|
${shlibs:Depends},
|
||||||
${synapse:pydepends},
|
${synapse:pydepends},
|
||||||
|
@ -50,17 +50,22 @@ FROM ${distro}
|
|||||||
ARG distro=""
|
ARG distro=""
|
||||||
ENV distro ${distro}
|
ENV distro ${distro}
|
||||||
|
|
||||||
|
# Python < 3.7 assumes LANG="C" means ASCII-only and throws on printing unicode
|
||||||
|
# http://bugs.python.org/issue19846
|
||||||
|
ENV LANG C.UTF-8
|
||||||
|
|
||||||
# Install the build dependencies
|
# Install the build dependencies
|
||||||
#
|
#
|
||||||
# NB: keep this list in sync with the list of build-deps in debian/control
|
# NB: keep this list in sync with the list of build-deps in debian/control
|
||||||
# TODO: it would be nice to do that automatically.
|
# TODO: it would be nice to do that automatically.
|
||||||
|
# TODO: Remove the dh-systemd stanza after dropping support for Ubuntu xenial
|
||||||
|
# it's a transitional package on all other, more recent releases
|
||||||
RUN apt-get update -qq -o Acquire::Languages=none \
|
RUN apt-get update -qq -o Acquire::Languages=none \
|
||||||
&& env DEBIAN_FRONTEND=noninteractive apt-get install \
|
&& env DEBIAN_FRONTEND=noninteractive apt-get install \
|
||||||
-yqq --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \
|
-yqq --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \
|
||||||
build-essential \
|
build-essential \
|
||||||
debhelper \
|
debhelper \
|
||||||
devscripts \
|
devscripts \
|
||||||
dh-systemd \
|
|
||||||
libsystemd-dev \
|
libsystemd-dev \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@ -70,7 +75,10 @@ RUN apt-get update -qq -o Acquire::Languages=none \
|
|||||||
python3-venv \
|
python3-venv \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
xmlsec1
|
xmlsec1 \
|
||||||
|
&& ( env DEBIAN_FRONTEND=noninteractive apt-get install \
|
||||||
|
-yqq --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \
|
||||||
|
dh-systemd || true )
|
||||||
|
|
||||||
COPY --from=builder /dh-virtualenv_1.2~dev-1_all.deb /
|
COPY --from=builder /dh-virtualenv_1.2~dev-1_all.deb /
|
||||||
|
|
||||||
|
@ -198,12 +198,10 @@ old_signing_keys: {}
|
|||||||
key_refresh_interval: "1d" # 1 Day.
|
key_refresh_interval: "1d" # 1 Day.
|
||||||
|
|
||||||
# The trusted servers to download signing keys from.
|
# The trusted servers to download signing keys from.
|
||||||
perspectives:
|
trusted_key_servers:
|
||||||
servers:
|
- server_name: matrix.org
|
||||||
"matrix.org":
|
verify_keys:
|
||||||
verify_keys:
|
"ed25519:auto": "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
|
||||||
"ed25519:auto":
|
|
||||||
key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
|
|
||||||
|
|
||||||
password_config:
|
password_config:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
* [Quarantining media by ID](#quarantining-media-by-id)
|
* [Quarantining media by ID](#quarantining-media-by-id)
|
||||||
* [Quarantining media in a room](#quarantining-media-in-a-room)
|
* [Quarantining media in a room](#quarantining-media-in-a-room)
|
||||||
* [Quarantining all media of a user](#quarantining-all-media-of-a-user)
|
* [Quarantining all media of a user](#quarantining-all-media-of-a-user)
|
||||||
|
* [Protecting media from being quarantined](#protecting-media-from-being-quarantined)
|
||||||
- [Delete local media](#delete-local-media)
|
- [Delete local media](#delete-local-media)
|
||||||
* [Delete a specific local media](#delete-a-specific-local-media)
|
* [Delete a specific local media](#delete-a-specific-local-media)
|
||||||
* [Delete local media by date or size](#delete-local-media-by-date-or-size)
|
* [Delete local media by date or size](#delete-local-media-by-date-or-size)
|
||||||
@ -123,6 +124,29 @@ The following fields are returned in the JSON response body:
|
|||||||
|
|
||||||
* `num_quarantined`: integer - The number of media items successfully quarantined
|
* `num_quarantined`: integer - The number of media items successfully quarantined
|
||||||
|
|
||||||
|
## Protecting media from being quarantined
|
||||||
|
|
||||||
|
This API protects a single piece of local media from being quarantined using the
|
||||||
|
above APIs. This is useful for sticker packs and other shared media which you do
|
||||||
|
not want to get quarantined, especially when
|
||||||
|
[quarantining media in a room](#quarantining-media-in-a-room).
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /_synapse/admin/v1/media/protect/<media_id>
|
||||||
|
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `media_id` is in the form of `abcdefg12345...`.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
# Delete local media
|
# Delete local media
|
||||||
This API deletes the *local* media from the disk of your own server.
|
This API deletes the *local* media from the disk of your own server.
|
||||||
This includes any local thumbnails and copies of media downloaded from
|
This includes any local thumbnails and copies of media downloaded from
|
||||||
|
@ -98,6 +98,8 @@ Body parameters:
|
|||||||
|
|
||||||
- ``deactivated``, optional. If unspecified, deactivation state will be left
|
- ``deactivated``, optional. If unspecified, deactivation state will be left
|
||||||
unchanged on existing accounts and set to ``false`` for new accounts.
|
unchanged on existing accounts and set to ``false`` for new accounts.
|
||||||
|
A user cannot be erased by deactivating with this API. For details on deactivating users see
|
||||||
|
`Deactivate Account <#deactivate-account>`_.
|
||||||
|
|
||||||
If the user already exists then optional parameters default to the current value.
|
If the user already exists then optional parameters default to the current value.
|
||||||
|
|
||||||
@ -248,6 +250,25 @@ server admin: see `README.rst <README.rst>`_.
|
|||||||
The erase parameter is optional and defaults to ``false``.
|
The erase parameter is optional and defaults to ``false``.
|
||||||
An empty body may be passed for backwards compatibility.
|
An empty body may be passed for backwards compatibility.
|
||||||
|
|
||||||
|
The following actions are performed when deactivating an user:
|
||||||
|
|
||||||
|
- Try to unpind 3PIDs from the identity server
|
||||||
|
- Remove all 3PIDs from the homeserver
|
||||||
|
- Delete all devices and E2EE keys
|
||||||
|
- Delete all access tokens
|
||||||
|
- Delete the password hash
|
||||||
|
- Removal from all rooms the user is a member of
|
||||||
|
- Remove the user from the user directory
|
||||||
|
- Reject all pending invites
|
||||||
|
- Remove all account validity information related to the user
|
||||||
|
|
||||||
|
The following additional actions are performed during deactivation if``erase``
|
||||||
|
is set to ``true``:
|
||||||
|
|
||||||
|
- Remove the user's display name
|
||||||
|
- Remove the user's avatar URL
|
||||||
|
- Mark the user as erased
|
||||||
|
|
||||||
|
|
||||||
Reset password
|
Reset password
|
||||||
==============
|
==============
|
||||||
@ -337,6 +358,10 @@ A response body like the following is returned:
|
|||||||
"total": 2
|
"total": 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
The server returns the list of rooms of which the user and the server
|
||||||
|
are member. If the user is local, all the rooms of which the user is
|
||||||
|
member are returned.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
The following parameters should be set in the URL:
|
The following parameters should be set in the URL:
|
||||||
|
32
docs/auth_chain_diff.dot
Normal file
32
docs/auth_chain_diff.dot
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
digraph auth {
|
||||||
|
nodesep=0.5;
|
||||||
|
rankdir="RL";
|
||||||
|
|
||||||
|
C [label="Create (1,1)"];
|
||||||
|
|
||||||
|
BJ [label="Bob's Join (2,1)", color=red];
|
||||||
|
BJ2 [label="Bob's Join (2,2)", color=red];
|
||||||
|
BJ2 -> BJ [color=red, dir=none];
|
||||||
|
|
||||||
|
subgraph cluster_foo {
|
||||||
|
A1 [label="Alice's invite (4,1)", color=blue];
|
||||||
|
A2 [label="Alice's Join (4,2)", color=blue];
|
||||||
|
A3 [label="Alice's Join (4,3)", color=blue];
|
||||||
|
A3 -> A2 -> A1 [color=blue, dir=none];
|
||||||
|
color=none;
|
||||||
|
}
|
||||||
|
|
||||||
|
PL1 [label="Power Level (3,1)", color=darkgreen];
|
||||||
|
PL2 [label="Power Level (3,2)", color=darkgreen];
|
||||||
|
PL2 -> PL1 [color=darkgreen, dir=none];
|
||||||
|
|
||||||
|
{rank = same; C; BJ; PL1; A1;}
|
||||||
|
|
||||||
|
A1 -> C [color=grey];
|
||||||
|
A1 -> BJ [color=grey];
|
||||||
|
PL1 -> C [color=grey];
|
||||||
|
BJ2 -> PL1 [penwidth=2];
|
||||||
|
|
||||||
|
A3 -> PL2 [penwidth=2];
|
||||||
|
A1 -> PL1 -> BJ -> C [penwidth=2];
|
||||||
|
}
|
BIN
docs/auth_chain_diff.dot.png
Normal file
BIN
docs/auth_chain_diff.dot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
108
docs/auth_chain_difference_algorithm.md
Normal file
108
docs/auth_chain_difference_algorithm.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Auth Chain Difference Algorithm
|
||||||
|
|
||||||
|
The auth chain difference algorithm is used by V2 state resolution, where a
|
||||||
|
naive implementation can be a significant source of CPU and DB usage.
|
||||||
|
|
||||||
|
### Definitions
|
||||||
|
|
||||||
|
A *state set* is a set of state events; e.g. the input of a state resolution
|
||||||
|
algorithm is a collection of state sets.
|
||||||
|
|
||||||
|
The *auth chain* of a set of events are all the events' auth events and *their*
|
||||||
|
auth events, recursively (i.e. the events reachable by walking the graph induced
|
||||||
|
by an event's auth events links).
|
||||||
|
|
||||||
|
The *auth chain difference* of a collection of state sets is the union minus the
|
||||||
|
intersection of the sets of auth chains corresponding to the state sets, i.e an
|
||||||
|
event is in the auth chain difference if it is reachable by walking the auth
|
||||||
|
event graph from at least one of the state sets but not from *all* of the state
|
||||||
|
sets.
|
||||||
|
|
||||||
|
## Breadth First Walk Algorithm
|
||||||
|
|
||||||
|
A way of calculating the auth chain difference without calculating the full auth
|
||||||
|
chains for each state set is to do a parallel breadth first walk (ordered by
|
||||||
|
depth) of each state set's auth chain. By tracking which events are reachable
|
||||||
|
from each state set we can finish early if every pending event is reachable from
|
||||||
|
every state set.
|
||||||
|
|
||||||
|
This can work well for state sets that have a small auth chain difference, but
|
||||||
|
can be very inefficient for larger differences. However, this algorithm is still
|
||||||
|
used if we don't have a chain cover index for the room (e.g. because we're in
|
||||||
|
the process of indexing it).
|
||||||
|
|
||||||
|
## Chain Cover Index
|
||||||
|
|
||||||
|
Synapse computes auth chain differences by pre-computing a "chain cover" index
|
||||||
|
for the auth chain in a room, allowing efficient reachability queries like "is
|
||||||
|
event A in the auth chain of event B". This is done by assigning every event a
|
||||||
|
*chain ID* and *sequence number* (e.g. `(5,3)`), and having a map of *links*
|
||||||
|
between chains (e.g. `(5,3) -> (2,4)`) such that A is reachable by B (i.e. `A`
|
||||||
|
is in the auth chain of `B`) if and only if either:
|
||||||
|
|
||||||
|
1. A and B have the same chain ID and `A`'s sequence number is less than `B`'s
|
||||||
|
sequence number; or
|
||||||
|
2. there is a link `L` between `B`'s chain ID and `A`'s chain ID such that
|
||||||
|
`L.start_seq_no` <= `B.seq_no` and `A.seq_no` <= `L.end_seq_no`.
|
||||||
|
|
||||||
|
There are actually two potential implementations, one where we store links from
|
||||||
|
each chain to every other reachable chain (the transitive closure of the links
|
||||||
|
graph), and one where we remove redundant links (the transitive reduction of the
|
||||||
|
links graph) e.g. if we have chains `C3 -> C2 -> C1` then the link `C3 -> C1`
|
||||||
|
would not be stored. Synapse uses the former implementations so that it doesn't
|
||||||
|
need to recurse to test reachability between chains.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
An example auth graph would look like the following, where chains have been
|
||||||
|
formed based on type/state_key and are denoted by colour and are labelled with
|
||||||
|
`(chain ID, sequence number)`. Links are denoted by the arrows (links in grey
|
||||||
|
are those that would be remove in the second implementation described above).
|
||||||
|
|
||||||
|
![Example](auth_chain_diff.dot.png)
|
||||||
|
|
||||||
|
Note that we don't include all links between events and their auth events, as
|
||||||
|
most of those links would be redundant. For example, all events point to the
|
||||||
|
create event, but each chain only needs the one link from it's base to the
|
||||||
|
create event.
|
||||||
|
|
||||||
|
## Using the Index
|
||||||
|
|
||||||
|
This index can be used to calculate the auth chain difference of the state sets
|
||||||
|
by looking at the chain ID and sequence numbers reachable from each state set:
|
||||||
|
|
||||||
|
1. For every state set lookup the chain ID/sequence numbers of each state event
|
||||||
|
2. Use the index to find all chains and the maximum sequence number reachable
|
||||||
|
from each state set.
|
||||||
|
3. The auth chain difference is then all events in each chain that have sequence
|
||||||
|
numbers between the maximum sequence number reachable from *any* state set and
|
||||||
|
the minimum reachable by *all* state sets (if any).
|
||||||
|
|
||||||
|
Note that steps 2 is effectively calculating the auth chain for each state set
|
||||||
|
(in terms of chain IDs and sequence numbers), and step 3 is calculating the
|
||||||
|
difference between the union and intersection of the auth chains.
|
||||||
|
|
||||||
|
### Worked Example
|
||||||
|
|
||||||
|
For example, given the above graph, we can calculate the difference between
|
||||||
|
state sets consisting of:
|
||||||
|
|
||||||
|
1. `S1`: Alice's invite `(4,1)` and Bob's second join `(2,2)`; and
|
||||||
|
2. `S2`: Alice's second join `(4,3)` and Bob's first join `(2,1)`.
|
||||||
|
|
||||||
|
Using the index we see that the following auth chains are reachable from each
|
||||||
|
state set:
|
||||||
|
|
||||||
|
1. `S1`: `(1,1)`, `(2,2)`, `(3,1)` & `(4,1)`
|
||||||
|
2. `S2`: `(1,1)`, `(2,1)`, `(3,2)` & `(4,3)`
|
||||||
|
|
||||||
|
And so, for each the ranges that are in the auth chain difference:
|
||||||
|
1. Chain 1: None, (since everything can reach the create event).
|
||||||
|
2. Chain 2: The range `(1, 2]` (i.e. just `2`), as `1` is reachable by all state
|
||||||
|
sets and the maximum reachable is `2` (corresponding to Bob's second join).
|
||||||
|
3. Chain 3: Similarly the range `(1, 2]` (corresponding to the second power
|
||||||
|
level).
|
||||||
|
4. Chain 4: The range `(1, 3]` (corresponding to both of Alice's joins).
|
||||||
|
|
||||||
|
So the final result is: Bob's second join `(2,2)`, the second power level
|
||||||
|
`(3,2)` and both of Alice's joins `(4,2)` & `(4,3)`.
|
199
docs/openid.md
199
docs/openid.md
@ -42,11 +42,10 @@ as follows:
|
|||||||
* For other installation mechanisms, see the documentation provided by the
|
* For other installation mechanisms, see the documentation provided by the
|
||||||
maintainer.
|
maintainer.
|
||||||
|
|
||||||
To enable the OpenID integration, you should then add an `oidc_config` section
|
To enable the OpenID integration, you should then add a section to the `oidc_providers`
|
||||||
to your configuration file (or uncomment the `enabled: true` line in the
|
setting in your configuration file (or uncomment one of the existing examples).
|
||||||
existing section). See [sample_config.yaml](./sample_config.yaml) for some
|
See [sample_config.yaml](./sample_config.yaml) for some sample settings, as well as
|
||||||
sample settings, as well as the text below for example configurations for
|
the text below for example configurations for specific providers.
|
||||||
specific providers.
|
|
||||||
|
|
||||||
## Sample configs
|
## Sample configs
|
||||||
|
|
||||||
@ -62,20 +61,21 @@ Directory (tenant) ID as it will be used in the Azure links.
|
|||||||
Edit your Synapse config file and change the `oidc_config` section:
|
Edit your Synapse config file and change the `oidc_config` section:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: microsoft
|
||||||
issuer: "https://login.microsoftonline.com/<tenant id>/v2.0"
|
idp_name: Microsoft
|
||||||
client_id: "<client id>"
|
issuer: "https://login.microsoftonline.com/<tenant id>/v2.0"
|
||||||
client_secret: "<client secret>"
|
client_id: "<client id>"
|
||||||
scopes: ["openid", "profile"]
|
client_secret: "<client secret>"
|
||||||
authorization_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize"
|
scopes: ["openid", "profile"]
|
||||||
token_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token"
|
authorization_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize"
|
||||||
userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo"
|
token_endpoint: "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token"
|
||||||
|
userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo"
|
||||||
|
|
||||||
user_mapping_provider:
|
user_mapping_provider:
|
||||||
config:
|
config:
|
||||||
localpart_template: "{{ user.preferred_username.split('@')[0] }}"
|
localpart_template: "{{ user.preferred_username.split('@')[0] }}"
|
||||||
display_name_template: "{{ user.name }}"
|
display_name_template: "{{ user.name }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Dex][dex-idp]
|
### [Dex][dex-idp]
|
||||||
@ -103,17 +103,18 @@ Run with `dex serve examples/config-dev.yaml`.
|
|||||||
Synapse config:
|
Synapse config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: dex
|
||||||
skip_verification: true # This is needed as Dex is served on an insecure endpoint
|
idp_name: "My Dex server"
|
||||||
issuer: "http://127.0.0.1:5556/dex"
|
skip_verification: true # This is needed as Dex is served on an insecure endpoint
|
||||||
client_id: "synapse"
|
issuer: "http://127.0.0.1:5556/dex"
|
||||||
client_secret: "secret"
|
client_id: "synapse"
|
||||||
scopes: ["openid", "profile"]
|
client_secret: "secret"
|
||||||
user_mapping_provider:
|
scopes: ["openid", "profile"]
|
||||||
config:
|
user_mapping_provider:
|
||||||
localpart_template: "{{ user.name }}"
|
config:
|
||||||
display_name_template: "{{ user.name|capitalize }}"
|
localpart_template: "{{ user.name }}"
|
||||||
|
display_name_template: "{{ user.name|capitalize }}"
|
||||||
```
|
```
|
||||||
### [Keycloak][keycloak-idp]
|
### [Keycloak][keycloak-idp]
|
||||||
|
|
||||||
@ -152,16 +153,17 @@ Follow the [Getting Started Guide](https://www.keycloak.org/getting-started) to
|
|||||||
8. Copy Secret
|
8. Copy Secret
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: keycloak
|
||||||
issuer: "https://127.0.0.1:8443/auth/realms/{realm_name}"
|
idp_name: "My KeyCloak server"
|
||||||
client_id: "synapse"
|
issuer: "https://127.0.0.1:8443/auth/realms/{realm_name}"
|
||||||
client_secret: "copy secret generated from above"
|
client_id: "synapse"
|
||||||
scopes: ["openid", "profile"]
|
client_secret: "copy secret generated from above"
|
||||||
user_mapping_provider:
|
scopes: ["openid", "profile"]
|
||||||
config:
|
user_mapping_provider:
|
||||||
localpart_template: "{{ user.preferred_username }}"
|
config:
|
||||||
display_name_template: "{{ user.name }}"
|
localpart_template: "{{ user.preferred_username }}"
|
||||||
|
display_name_template: "{{ user.name }}"
|
||||||
```
|
```
|
||||||
### [Auth0][auth0]
|
### [Auth0][auth0]
|
||||||
|
|
||||||
@ -191,16 +193,17 @@ oidc_config:
|
|||||||
Synapse config:
|
Synapse config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: auth0
|
||||||
issuer: "https://your-tier.eu.auth0.com/" # TO BE FILLED
|
idp_name: Auth0
|
||||||
client_id: "your-client-id" # TO BE FILLED
|
issuer: "https://your-tier.eu.auth0.com/" # TO BE FILLED
|
||||||
client_secret: "your-client-secret" # TO BE FILLED
|
client_id: "your-client-id" # TO BE FILLED
|
||||||
scopes: ["openid", "profile"]
|
client_secret: "your-client-secret" # TO BE FILLED
|
||||||
user_mapping_provider:
|
scopes: ["openid", "profile"]
|
||||||
config:
|
user_mapping_provider:
|
||||||
localpart_template: "{{ user.preferred_username }}"
|
config:
|
||||||
display_name_template: "{{ user.name }}"
|
localpart_template: "{{ user.preferred_username }}"
|
||||||
|
display_name_template: "{{ user.name }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
### GitHub
|
### GitHub
|
||||||
@ -219,21 +222,22 @@ does not return a `sub` property, an alternative `subject_claim` has to be set.
|
|||||||
Synapse config:
|
Synapse config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: github
|
||||||
discover: false
|
idp_name: Github
|
||||||
issuer: "https://github.com/"
|
discover: false
|
||||||
client_id: "your-client-id" # TO BE FILLED
|
issuer: "https://github.com/"
|
||||||
client_secret: "your-client-secret" # TO BE FILLED
|
client_id: "your-client-id" # TO BE FILLED
|
||||||
authorization_endpoint: "https://github.com/login/oauth/authorize"
|
client_secret: "your-client-secret" # TO BE FILLED
|
||||||
token_endpoint: "https://github.com/login/oauth/access_token"
|
authorization_endpoint: "https://github.com/login/oauth/authorize"
|
||||||
userinfo_endpoint: "https://api.github.com/user"
|
token_endpoint: "https://github.com/login/oauth/access_token"
|
||||||
scopes: ["read:user"]
|
userinfo_endpoint: "https://api.github.com/user"
|
||||||
user_mapping_provider:
|
scopes: ["read:user"]
|
||||||
config:
|
user_mapping_provider:
|
||||||
subject_claim: "id"
|
config:
|
||||||
localpart_template: "{{ user.login }}"
|
subject_claim: "id"
|
||||||
display_name_template: "{{ user.name }}"
|
localpart_template: "{{ user.login }}"
|
||||||
|
display_name_template: "{{ user.name }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
### [Google][google-idp]
|
### [Google][google-idp]
|
||||||
@ -243,16 +247,17 @@ oidc_config:
|
|||||||
2. add an "OAuth Client ID" for a Web Application under "Credentials".
|
2. add an "OAuth Client ID" for a Web Application under "Credentials".
|
||||||
3. Copy the Client ID and Client Secret, and add the following to your synapse config:
|
3. Copy the Client ID and Client Secret, and add the following to your synapse config:
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: google
|
||||||
issuer: "https://accounts.google.com/"
|
idp_name: Google
|
||||||
client_id: "your-client-id" # TO BE FILLED
|
issuer: "https://accounts.google.com/"
|
||||||
client_secret: "your-client-secret" # TO BE FILLED
|
client_id: "your-client-id" # TO BE FILLED
|
||||||
scopes: ["openid", "profile"]
|
client_secret: "your-client-secret" # TO BE FILLED
|
||||||
user_mapping_provider:
|
scopes: ["openid", "profile"]
|
||||||
config:
|
user_mapping_provider:
|
||||||
localpart_template: "{{ user.given_name|lower }}"
|
config:
|
||||||
display_name_template: "{{ user.name }}"
|
localpart_template: "{{ user.given_name|lower }}"
|
||||||
|
display_name_template: "{{ user.name }}"
|
||||||
```
|
```
|
||||||
4. Back in the Google console, add this Authorized redirect URI: `[synapse
|
4. Back in the Google console, add this Authorized redirect URI: `[synapse
|
||||||
public baseurl]/_synapse/oidc/callback`.
|
public baseurl]/_synapse/oidc/callback`.
|
||||||
@ -266,16 +271,17 @@ oidc_config:
|
|||||||
Synapse config:
|
Synapse config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: twitch
|
||||||
issuer: "https://id.twitch.tv/oauth2/"
|
idp_name: Twitch
|
||||||
client_id: "your-client-id" # TO BE FILLED
|
issuer: "https://id.twitch.tv/oauth2/"
|
||||||
client_secret: "your-client-secret" # TO BE FILLED
|
client_id: "your-client-id" # TO BE FILLED
|
||||||
client_auth_method: "client_secret_post"
|
client_secret: "your-client-secret" # TO BE FILLED
|
||||||
user_mapping_provider:
|
client_auth_method: "client_secret_post"
|
||||||
config:
|
user_mapping_provider:
|
||||||
localpart_template: "{{ user.preferred_username }}"
|
config:
|
||||||
display_name_template: "{{ user.name }}"
|
localpart_template: "{{ user.preferred_username }}"
|
||||||
|
display_name_template: "{{ user.name }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
### GitLab
|
### GitLab
|
||||||
@ -287,16 +293,17 @@ oidc_config:
|
|||||||
Synapse config:
|
Synapse config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
oidc_config:
|
oidc_providers:
|
||||||
enabled: true
|
- idp_id: gitlab
|
||||||
issuer: "https://gitlab.com/"
|
idp_name: Gitlab
|
||||||
client_id: "your-client-id" # TO BE FILLED
|
issuer: "https://gitlab.com/"
|
||||||
client_secret: "your-client-secret" # TO BE FILLED
|
client_id: "your-client-id" # TO BE FILLED
|
||||||
client_auth_method: "client_secret_post"
|
client_secret: "your-client-secret" # TO BE FILLED
|
||||||
scopes: ["openid", "read_user"]
|
client_auth_method: "client_secret_post"
|
||||||
user_profile_method: "userinfo_endpoint"
|
scopes: ["openid", "read_user"]
|
||||||
user_mapping_provider:
|
user_profile_method: "userinfo_endpoint"
|
||||||
config:
|
user_mapping_provider:
|
||||||
localpart_template: '{{ user.nickname }}'
|
config:
|
||||||
display_name_template: '{{ user.name }}'
|
localpart_template: '{{ user.nickname }}'
|
||||||
|
display_name_template: '{{ user.name }}'
|
||||||
```
|
```
|
||||||
|
@ -18,7 +18,7 @@ connect to a postgres database.
|
|||||||
virtualenv](../INSTALL.md#installing-from-source), you can install
|
virtualenv](../INSTALL.md#installing-from-source), you can install
|
||||||
the library with:
|
the library with:
|
||||||
|
|
||||||
~/synapse/env/bin/pip install matrix-synapse[postgres]
|
~/synapse/env/bin/pip install "matrix-synapse[postgres]"
|
||||||
|
|
||||||
(substituting the path to your virtualenv for `~/synapse/env`, if
|
(substituting the path to your virtualenv for `~/synapse/env`, if
|
||||||
you used a different path). You will require the postgres
|
you used a different path). You will require the postgres
|
||||||
|
@ -67,11 +67,16 @@ pid_file: DATADIR/homeserver.pid
|
|||||||
#
|
#
|
||||||
#web_client_location: https://riot.example.com/
|
#web_client_location: https://riot.example.com/
|
||||||
|
|
||||||
# The public-facing base URL that clients use to access this HS
|
# The public-facing base URL that clients use to access this Homeserver (not
|
||||||
# (not including _matrix/...). This is the same URL a user would
|
# including _matrix/...). This is the same URL a user might enter into the
|
||||||
# enter into the 'custom HS URL' field on their client. If you
|
# 'Custom Homeserver URL' field on their client. If you use Synapse with a
|
||||||
# use synapse with a reverse proxy, this should be the URL to reach
|
# reverse proxy, this should be the URL to reach Synapse via the proxy.
|
||||||
# synapse via the proxy.
|
# Otherwise, it should be the URL to reach Synapse's client HTTP listener (see
|
||||||
|
# 'listeners' below).
|
||||||
|
#
|
||||||
|
# If this is left unset, it defaults to 'https://<server_name>/'. (Note that
|
||||||
|
# that will not work unless you configure Synapse or a reverse-proxy to listen
|
||||||
|
# on port 443.)
|
||||||
#
|
#
|
||||||
#public_baseurl: https://example.com/
|
#public_baseurl: https://example.com/
|
||||||
|
|
||||||
@ -1150,8 +1155,9 @@ account_validity:
|
|||||||
# send an email to the account's email address with a renewal link. By
|
# send an email to the account's email address with a renewal link. By
|
||||||
# default, no such emails are sent.
|
# default, no such emails are sent.
|
||||||
#
|
#
|
||||||
# If you enable this setting, you will also need to fill out the 'email' and
|
# If you enable this setting, you will also need to fill out the 'email'
|
||||||
# 'public_baseurl' configuration sections.
|
# configuration section. You should also check that 'public_baseurl' is set
|
||||||
|
# correctly.
|
||||||
#
|
#
|
||||||
#renew_at: 1w
|
#renew_at: 1w
|
||||||
|
|
||||||
@ -1242,8 +1248,7 @@ account_validity:
|
|||||||
# The identity server which we suggest that clients should use when users log
|
# The identity server which we suggest that clients should use when users log
|
||||||
# in on this server.
|
# in on this server.
|
||||||
#
|
#
|
||||||
# (By default, no suggestion is made, so it is left up to the client.
|
# (By default, no suggestion is made, so it is left up to the client.)
|
||||||
# This setting is ignored unless public_baseurl is also set.)
|
|
||||||
#
|
#
|
||||||
#default_identity_server: https://matrix.org
|
#default_identity_server: https://matrix.org
|
||||||
|
|
||||||
@ -1268,8 +1273,6 @@ account_validity:
|
|||||||
# by the Matrix Identity Service API specification:
|
# by the Matrix Identity Service API specification:
|
||||||
# https://matrix.org/docs/spec/identity_service/latest
|
# https://matrix.org/docs/spec/identity_service/latest
|
||||||
#
|
#
|
||||||
# If a delegate is specified, the config option public_baseurl must also be filled out.
|
|
||||||
#
|
|
||||||
account_threepid_delegates:
|
account_threepid_delegates:
|
||||||
#email: https://example.com # Delegate email sending to example.com
|
#email: https://example.com # Delegate email sending to example.com
|
||||||
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
||||||
@ -1709,141 +1712,158 @@ saml2_config:
|
|||||||
#idp_entityid: 'https://our_idp/entityid'
|
#idp_entityid: 'https://our_idp/entityid'
|
||||||
|
|
||||||
|
|
||||||
# Enable OpenID Connect (OIDC) / OAuth 2.0 for registration and login.
|
# List of OpenID Connect (OIDC) / OAuth 2.0 identity providers, for registration
|
||||||
|
# and login.
|
||||||
|
#
|
||||||
|
# Options for each entry include:
|
||||||
|
#
|
||||||
|
# idp_id: a unique identifier for this identity provider. Used internally
|
||||||
|
# by Synapse; should be a single word such as 'github'.
|
||||||
|
#
|
||||||
|
# Note that, if this is changed, users authenticating via that provider
|
||||||
|
# will no longer be recognised as the same user!
|
||||||
|
#
|
||||||
|
# idp_name: A user-facing name for this identity provider, which is used to
|
||||||
|
# offer the user a choice of login mechanisms.
|
||||||
|
#
|
||||||
|
# idp_icon: An optional icon for this identity provider, which is presented
|
||||||
|
# by identity picker pages. If given, must be an MXC URI of the format
|
||||||
|
# mxc://<server-name>/<media-id>. (An easy way to obtain such an MXC URI
|
||||||
|
# is to upload an image to an (unencrypted) room and then copy the "url"
|
||||||
|
# from the source of the event.)
|
||||||
|
#
|
||||||
|
# discover: set to 'false' to disable the use of the OIDC discovery mechanism
|
||||||
|
# to discover endpoints. Defaults to true.
|
||||||
|
#
|
||||||
|
# issuer: Required. The OIDC issuer. Used to validate tokens and (if discovery
|
||||||
|
# is enabled) to discover the provider's endpoints.
|
||||||
|
#
|
||||||
|
# client_id: Required. oauth2 client id to use.
|
||||||
|
#
|
||||||
|
# client_secret: Required. oauth2 client secret to use.
|
||||||
|
#
|
||||||
|
# client_auth_method: auth method to use when exchanging the token. Valid
|
||||||
|
# values are 'client_secret_basic' (default), 'client_secret_post' and
|
||||||
|
# 'none'.
|
||||||
|
#
|
||||||
|
# scopes: list of scopes to request. This should normally include the "openid"
|
||||||
|
# scope. Defaults to ["openid"].
|
||||||
|
#
|
||||||
|
# authorization_endpoint: the oauth2 authorization endpoint. Required if
|
||||||
|
# provider discovery is disabled.
|
||||||
|
#
|
||||||
|
# token_endpoint: the oauth2 token endpoint. Required if provider discovery is
|
||||||
|
# disabled.
|
||||||
|
#
|
||||||
|
# userinfo_endpoint: the OIDC userinfo endpoint. Required if discovery is
|
||||||
|
# disabled and the 'openid' scope is not requested.
|
||||||
|
#
|
||||||
|
# jwks_uri: URI where to fetch the JWKS. Required if discovery is disabled and
|
||||||
|
# the 'openid' scope is used.
|
||||||
|
#
|
||||||
|
# skip_verification: set to 'true' to skip metadata verification. Use this if
|
||||||
|
# you are connecting to a provider that is not OpenID Connect compliant.
|
||||||
|
# Defaults to false. Avoid this in production.
|
||||||
|
#
|
||||||
|
# user_profile_method: Whether to fetch the user profile from the userinfo
|
||||||
|
# endpoint. Valid values are: 'auto' or 'userinfo_endpoint'.
|
||||||
|
#
|
||||||
|
# Defaults to 'auto', which fetches the userinfo endpoint if 'openid' is
|
||||||
|
# included in 'scopes'. Set to 'userinfo_endpoint' to always fetch the
|
||||||
|
# userinfo endpoint.
|
||||||
|
#
|
||||||
|
# allow_existing_users: set to 'true' to allow a user logging in via OIDC to
|
||||||
|
# match a pre-existing account instead of failing. This could be used if
|
||||||
|
# switching from password logins to OIDC. Defaults to false.
|
||||||
|
#
|
||||||
|
# user_mapping_provider: Configuration for how attributes returned from a OIDC
|
||||||
|
# provider are mapped onto a matrix user. This setting has the following
|
||||||
|
# sub-properties:
|
||||||
|
#
|
||||||
|
# module: The class name of a custom mapping module. Default is
|
||||||
|
# 'synapse.handlers.oidc_handler.JinjaOidcMappingProvider'.
|
||||||
|
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
|
||||||
|
# for information on implementing a custom mapping provider.
|
||||||
|
#
|
||||||
|
# config: Configuration for the mapping provider module. This section will
|
||||||
|
# be passed as a Python dictionary to the user mapping provider
|
||||||
|
# module's `parse_config` method.
|
||||||
|
#
|
||||||
|
# For the default provider, the following settings are available:
|
||||||
|
#
|
||||||
|
# sub: name of the claim containing a unique identifier for the
|
||||||
|
# user. Defaults to 'sub', which OpenID Connect compliant
|
||||||
|
# providers should provide.
|
||||||
|
#
|
||||||
|
# localpart_template: Jinja2 template for the localpart of the MXID.
|
||||||
|
# If this is not set, the user will be prompted to choose their
|
||||||
|
# own username.
|
||||||
|
#
|
||||||
|
# display_name_template: Jinja2 template for the display name to set
|
||||||
|
# on first login. If unset, no displayname will be set.
|
||||||
|
#
|
||||||
|
# extra_attributes: a map of Jinja2 templates for extra attributes
|
||||||
|
# to send back to the client during login.
|
||||||
|
# Note that these are non-standard and clients will ignore them
|
||||||
|
# without modifications.
|
||||||
|
#
|
||||||
|
# When rendering, the Jinja2 templates are given a 'user' variable,
|
||||||
|
# which is set to the claims returned by the UserInfo Endpoint and/or
|
||||||
|
# in the ID Token.
|
||||||
#
|
#
|
||||||
# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
|
# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
|
||||||
# for some example configurations.
|
# for information on how to configure these options.
|
||||||
#
|
#
|
||||||
oidc_config:
|
# For backwards compatibility, it is also possible to configure a single OIDC
|
||||||
# Uncomment the following to enable authorization against an OpenID Connect
|
# provider via an 'oidc_config' setting. This is now deprecated and admins are
|
||||||
# server. Defaults to false.
|
# advised to migrate to the 'oidc_providers' format. (When doing that migration,
|
||||||
|
# use 'oidc' for the idp_id to ensure that existing users continue to be
|
||||||
|
# recognised.)
|
||||||
|
#
|
||||||
|
oidc_providers:
|
||||||
|
# Generic example
|
||||||
#
|
#
|
||||||
#enabled: true
|
#- idp_id: my_idp
|
||||||
|
# idp_name: "My OpenID provider"
|
||||||
|
# idp_icon: "mxc://example.com/mediaid"
|
||||||
|
# discover: false
|
||||||
|
# issuer: "https://accounts.example.com/"
|
||||||
|
# client_id: "provided-by-your-issuer"
|
||||||
|
# client_secret: "provided-by-your-issuer"
|
||||||
|
# client_auth_method: client_secret_post
|
||||||
|
# scopes: ["openid", "profile"]
|
||||||
|
# authorization_endpoint: "https://accounts.example.com/oauth2/auth"
|
||||||
|
# token_endpoint: "https://accounts.example.com/oauth2/token"
|
||||||
|
# userinfo_endpoint: "https://accounts.example.com/userinfo"
|
||||||
|
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
|
||||||
|
# skip_verification: true
|
||||||
|
|
||||||
# Uncomment the following to disable use of the OIDC discovery mechanism to
|
# For use with Keycloak
|
||||||
# discover endpoints. Defaults to true.
|
|
||||||
#
|
#
|
||||||
#discover: false
|
#- idp_id: keycloak
|
||||||
|
# idp_name: Keycloak
|
||||||
|
# issuer: "https://127.0.0.1:8443/auth/realms/my_realm_name"
|
||||||
|
# client_id: "synapse"
|
||||||
|
# client_secret: "copy secret generated in Keycloak UI"
|
||||||
|
# scopes: ["openid", "profile"]
|
||||||
|
|
||||||
# the OIDC issuer. Used to validate tokens and (if discovery is enabled) to
|
# For use with Github
|
||||||
# discover the provider's endpoints.
|
|
||||||
#
|
#
|
||||||
# Required if 'enabled' is true.
|
#- idp_id: github
|
||||||
#
|
# idp_name: Github
|
||||||
#issuer: "https://accounts.example.com/"
|
# discover: false
|
||||||
|
# issuer: "https://github.com/"
|
||||||
# oauth2 client id to use.
|
# client_id: "your-client-id" # TO BE FILLED
|
||||||
#
|
# client_secret: "your-client-secret" # TO BE FILLED
|
||||||
# Required if 'enabled' is true.
|
# authorization_endpoint: "https://github.com/login/oauth/authorize"
|
||||||
#
|
# token_endpoint: "https://github.com/login/oauth/access_token"
|
||||||
#client_id: "provided-by-your-issuer"
|
# userinfo_endpoint: "https://api.github.com/user"
|
||||||
|
# scopes: ["read:user"]
|
||||||
# oauth2 client secret to use.
|
# user_mapping_provider:
|
||||||
#
|
# config:
|
||||||
# Required if 'enabled' is true.
|
# subject_claim: "id"
|
||||||
#
|
# localpart_template: "{ user.login }"
|
||||||
#client_secret: "provided-by-your-issuer"
|
# display_name_template: "{ user.name }"
|
||||||
|
|
||||||
# auth method to use when exchanging the token.
|
|
||||||
# Valid values are 'client_secret_basic' (default), 'client_secret_post' and
|
|
||||||
# 'none'.
|
|
||||||
#
|
|
||||||
#client_auth_method: client_secret_post
|
|
||||||
|
|
||||||
# list of scopes to request. This should normally include the "openid" scope.
|
|
||||||
# Defaults to ["openid"].
|
|
||||||
#
|
|
||||||
#scopes: ["openid", "profile"]
|
|
||||||
|
|
||||||
# the oauth2 authorization endpoint. Required if provider discovery is disabled.
|
|
||||||
#
|
|
||||||
#authorization_endpoint: "https://accounts.example.com/oauth2/auth"
|
|
||||||
|
|
||||||
# the oauth2 token endpoint. Required if provider discovery is disabled.
|
|
||||||
#
|
|
||||||
#token_endpoint: "https://accounts.example.com/oauth2/token"
|
|
||||||
|
|
||||||
# the OIDC userinfo endpoint. Required if discovery is disabled and the
|
|
||||||
# "openid" scope is not requested.
|
|
||||||
#
|
|
||||||
#userinfo_endpoint: "https://accounts.example.com/userinfo"
|
|
||||||
|
|
||||||
# URI where to fetch the JWKS. Required if discovery is disabled and the
|
|
||||||
# "openid" scope is used.
|
|
||||||
#
|
|
||||||
#jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
|
|
||||||
|
|
||||||
# Uncomment to skip metadata verification. Defaults to false.
|
|
||||||
#
|
|
||||||
# Use this if you are connecting to a provider that is not OpenID Connect
|
|
||||||
# compliant.
|
|
||||||
# Avoid this in production.
|
|
||||||
#
|
|
||||||
#skip_verification: true
|
|
||||||
|
|
||||||
# Whether to fetch the user profile from the userinfo endpoint. Valid
|
|
||||||
# values are: "auto" or "userinfo_endpoint".
|
|
||||||
#
|
|
||||||
# Defaults to "auto", which fetches the userinfo endpoint if "openid" is included
|
|
||||||
# in `scopes`. Uncomment the following to always fetch the userinfo endpoint.
|
|
||||||
#
|
|
||||||
#user_profile_method: "userinfo_endpoint"
|
|
||||||
|
|
||||||
# Uncomment to allow a user logging in via OIDC to match a pre-existing account instead
|
|
||||||
# of failing. This could be used if switching from password logins to OIDC. Defaults to false.
|
|
||||||
#
|
|
||||||
#allow_existing_users: true
|
|
||||||
|
|
||||||
# An external module can be provided here as a custom solution to mapping
|
|
||||||
# attributes returned from a OIDC provider onto a matrix user.
|
|
||||||
#
|
|
||||||
user_mapping_provider:
|
|
||||||
# The custom module's class. Uncomment to use a custom module.
|
|
||||||
# Default is 'synapse.handlers.oidc_handler.JinjaOidcMappingProvider'.
|
|
||||||
#
|
|
||||||
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
|
|
||||||
# for information on implementing a custom mapping provider.
|
|
||||||
#
|
|
||||||
#module: mapping_provider.OidcMappingProvider
|
|
||||||
|
|
||||||
# Custom configuration values for the module. This section will be passed as
|
|
||||||
# a Python dictionary to the user mapping provider module's `parse_config`
|
|
||||||
# method.
|
|
||||||
#
|
|
||||||
# The examples below are intended for the default provider: they should be
|
|
||||||
# changed if using a custom provider.
|
|
||||||
#
|
|
||||||
config:
|
|
||||||
# name of the claim containing a unique identifier for the user.
|
|
||||||
# Defaults to `sub`, which OpenID Connect compliant providers should provide.
|
|
||||||
#
|
|
||||||
#subject_claim: "sub"
|
|
||||||
|
|
||||||
# Jinja2 template for the localpart of the MXID.
|
|
||||||
#
|
|
||||||
# When rendering, this template is given the following variables:
|
|
||||||
# * user: The claims returned by the UserInfo Endpoint and/or in the ID
|
|
||||||
# Token
|
|
||||||
#
|
|
||||||
# If this is not set, the user will be prompted to choose their
|
|
||||||
# own username.
|
|
||||||
#
|
|
||||||
#localpart_template: "{{ user.preferred_username }}"
|
|
||||||
|
|
||||||
# Jinja2 template for the display name to set on first login.
|
|
||||||
#
|
|
||||||
# If unset, no displayname will be set.
|
|
||||||
#
|
|
||||||
#display_name_template: "{{ user.given_name }} {{ user.last_name }}"
|
|
||||||
|
|
||||||
# Jinja2 templates for extra attributes to send back to the client during
|
|
||||||
# login.
|
|
||||||
#
|
|
||||||
# Note that these are non-standard and clients will ignore them without modifications.
|
|
||||||
#
|
|
||||||
#extra_attributes:
|
|
||||||
#birthdate: "{{ user.birthdate }}"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Enable Central Authentication Service (CAS) for registration and login.
|
# Enable Central Authentication Service (CAS) for registration and login.
|
||||||
@ -1893,9 +1913,9 @@ sso:
|
|||||||
# phishing attacks from evil.site. To avoid this, include a slash after the
|
# phishing attacks from evil.site. To avoid this, include a slash after the
|
||||||
# hostname: "https://my.client/".
|
# hostname: "https://my.client/".
|
||||||
#
|
#
|
||||||
# If public_baseurl is set, then the login fallback page (used by clients
|
# The login fallback page (used by clients that don't natively support the
|
||||||
# that don't natively support the required login flows) is whitelisted in
|
# required login flows) is automatically whitelisted in addition to any URLs
|
||||||
# addition to any URLs in this list.
|
# in this list.
|
||||||
#
|
#
|
||||||
# By default, this list is empty.
|
# By default, this list is empty.
|
||||||
#
|
#
|
||||||
@ -1969,6 +1989,14 @@ sso:
|
|||||||
#
|
#
|
||||||
# This template has no additional variables.
|
# This template has no additional variables.
|
||||||
#
|
#
|
||||||
|
# * HTML page shown after a user-interactive authentication session which
|
||||||
|
# does not map correctly onto the expected user: 'sso_auth_bad_user.html'.
|
||||||
|
#
|
||||||
|
# When rendering, this template is given the following variables:
|
||||||
|
# * server_name: the homeserver's name.
|
||||||
|
# * user_id_to_verify: the MXID of the user that we are trying to
|
||||||
|
# validate.
|
||||||
|
#
|
||||||
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
|
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
|
||||||
# attempts to login: 'sso_account_deactivated.html'.
|
# attempts to login: 'sso_account_deactivated.html'.
|
||||||
#
|
#
|
||||||
|
@ -232,6 +232,12 @@ Here are a few things to try:
|
|||||||
|
|
||||||
(Understanding the output is beyond the scope of this document!)
|
(Understanding the output is beyond the scope of this document!)
|
||||||
|
|
||||||
|
* You can test your Matrix homeserver TURN setup with https://test.voip.librepush.net/.
|
||||||
|
Note that this test is not fully reliable yet, so don't be discouraged if
|
||||||
|
the test fails.
|
||||||
|
[Here](https://github.com/matrix-org/voip-tester) is the github repo of the
|
||||||
|
source of the tester, where you can file bug reports.
|
||||||
|
|
||||||
* There is a WebRTC test tool at
|
* There is a WebRTC test tool at
|
||||||
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/. To
|
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/. To
|
||||||
use it, you will need a username/password for your TURN server. You can
|
use it, you will need a username/password for your TURN server. You can
|
||||||
|
@ -16,6 +16,9 @@ workers only work with PostgreSQL-based Synapse deployments. SQLite should only
|
|||||||
be used for demo purposes and any admin considering workers should already be
|
be used for demo purposes and any admin considering workers should already be
|
||||||
running PostgreSQL.
|
running PostgreSQL.
|
||||||
|
|
||||||
|
See also https://matrix.org/blog/2020/11/03/how-we-fixed-synapses-scalability
|
||||||
|
for a higher level overview.
|
||||||
|
|
||||||
## Main process/worker communication
|
## Main process/worker communication
|
||||||
|
|
||||||
The processes communicate with each other via a Synapse-specific protocol called
|
The processes communicate with each other via a Synapse-specific protocol called
|
||||||
@ -56,7 +59,7 @@ The appropriate dependencies must also be installed for Synapse. If using a
|
|||||||
virtualenv, these can be installed with:
|
virtualenv, these can be installed with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install matrix-synapse[redis]
|
pip install "matrix-synapse[redis]"
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that these dependencies are included when synapse is installed with `pip
|
Note that these dependencies are included when synapse is installed with `pip
|
||||||
@ -214,6 +217,7 @@ expressions:
|
|||||||
^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/members$
|
^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/members$
|
||||||
^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/state$
|
^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/state$
|
||||||
^/_matrix/client/(api/v1|r0|unstable)/account/3pid$
|
^/_matrix/client/(api/v1|r0|unstable)/account/3pid$
|
||||||
|
^/_matrix/client/(api/v1|r0|unstable)/devices$
|
||||||
^/_matrix/client/(api/v1|r0|unstable)/keys/query$
|
^/_matrix/client/(api/v1|r0|unstable)/keys/query$
|
||||||
^/_matrix/client/(api/v1|r0|unstable)/keys/changes$
|
^/_matrix/client/(api/v1|r0|unstable)/keys/changes$
|
||||||
^/_matrix/client/versions$
|
^/_matrix/client/versions$
|
||||||
|
1
mypy.ini
1
mypy.ini
@ -100,6 +100,7 @@ files =
|
|||||||
synapse/util/async_helpers.py,
|
synapse/util/async_helpers.py,
|
||||||
synapse/util/caches,
|
synapse/util/caches,
|
||||||
synapse/util/metrics.py,
|
synapse/util/metrics.py,
|
||||||
|
synapse/util/stringutils.py,
|
||||||
tests/replication,
|
tests/replication,
|
||||||
tests/test_utils,
|
tests/test_utils,
|
||||||
tests/handlers/test_password_providers.py,
|
tests/handlers/test_password_providers.py,
|
||||||
|
@ -80,7 +80,8 @@ else
|
|||||||
# then lint everything!
|
# then lint everything!
|
||||||
if [[ -z ${files+x} ]]; then
|
if [[ -z ${files+x} ]]; then
|
||||||
# Lint all source code files and directories
|
# Lint all source code files and directories
|
||||||
files=("synapse" "tests" "scripts-dev" "scripts" "contrib" "synctl" "setup.py" "synmark")
|
# Note: this list aims the mirror the one in tox.ini
|
||||||
|
files=("synapse" "docker" "tests" "scripts-dev" "scripts" "contrib" "synctl" "setup.py" "synmark" "stubs" ".buildkite")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ logger = logging.getLogger("synapse_port_db")
|
|||||||
|
|
||||||
BOOLEAN_COLUMNS = {
|
BOOLEAN_COLUMNS = {
|
||||||
"events": ["processed", "outlier", "contains_url"],
|
"events": ["processed", "outlier", "contains_url"],
|
||||||
"rooms": ["is_public"],
|
"rooms": ["is_public", "has_auth_chain_index"],
|
||||||
"event_edges": ["is_state"],
|
"event_edges": ["is_state"],
|
||||||
"presence_list": ["accepted"],
|
"presence_list": ["accepted"],
|
||||||
"presence_stream": ["currently_active"],
|
"presence_stream": ["currently_active"],
|
||||||
|
1
setup.py
1
setup.py
@ -121,6 +121,7 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/x-rst",
|
||||||
python_requires="~=3.5",
|
python_requires="~=3.5",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
@ -48,7 +48,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
__version__ = "1.25.0rc1"
|
__version__ = "1.26.0rc1"
|
||||||
|
|
||||||
if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)):
|
if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)):
|
||||||
# We import here so that we don't have to install a bunch of deps when
|
# We import here so that we don't have to install a bunch of deps when
|
||||||
|
@ -33,6 +33,7 @@ from synapse.api.errors import (
|
|||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
|
from synapse.http import get_request_user_agent
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging import opentracing as opentracing
|
from synapse.logging import opentracing as opentracing
|
||||||
from synapse.storage.databases.main.registration import TokenLookupResult
|
from synapse.storage.databases.main.registration import TokenLookupResult
|
||||||
@ -186,8 +187,8 @@ class Auth:
|
|||||||
AuthError if access is denied for the user in the access token
|
AuthError if access is denied for the user in the access token
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
ip_addr = self.hs.get_ip_from_request(request)
|
ip_addr = request.getClientIP()
|
||||||
user_agent = request.get_user_agent("")
|
user_agent = get_request_user_agent(request)
|
||||||
|
|
||||||
access_token = self.get_access_token_from_request(request)
|
access_token = self.get_access_token_from_request(request)
|
||||||
|
|
||||||
@ -275,7 +276,7 @@ class Auth:
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
if app_service.ip_range_whitelist:
|
if app_service.ip_range_whitelist:
|
||||||
ip_address = IPAddress(self.hs.get_ip_from_request(request))
|
ip_address = IPAddress(request.getClientIP())
|
||||||
if ip_address not in app_service.ip_range_whitelist:
|
if ip_address not in app_service.ip_range_whitelist:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
@ -42,8 +42,6 @@ class ConsentURIBuilder:
|
|||||||
"""
|
"""
|
||||||
if hs_config.form_secret is None:
|
if hs_config.form_secret is None:
|
||||||
raise ConfigError("form_secret not set in config")
|
raise ConfigError("form_secret not set in config")
|
||||||
if hs_config.public_baseurl is None:
|
|
||||||
raise ConfigError("public_baseurl not set in config")
|
|
||||||
|
|
||||||
self._hmac_secret = hs_config.form_secret.encode("utf-8")
|
self._hmac_secret = hs_config.form_secret.encode("utf-8")
|
||||||
self._public_baseurl = hs_config.public_baseurl
|
self._public_baseurl = hs_config.public_baseurl
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2017 New Vector Ltd
|
# Copyright 2017 New Vector Ltd
|
||||||
|
# Copyright 2019-2021 The Matrix.org Foundation C.I.C
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -19,7 +20,7 @@ import signal
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Iterable
|
from typing import Awaitable, Callable, Iterable
|
||||||
|
|
||||||
from typing_extensions import NoReturn
|
from typing_extensions import NoReturn
|
||||||
|
|
||||||
@ -143,6 +144,45 @@ def quit_with_error(error_string: str) -> NoReturn:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def register_start(cb: Callable[..., Awaitable], *args, **kwargs) -> None:
|
||||||
|
"""Register a callback with the reactor, to be called once it is running
|
||||||
|
|
||||||
|
This can be used to initialise parts of the system which require an asynchronous
|
||||||
|
setup.
|
||||||
|
|
||||||
|
Any exception raised by the callback will be printed and logged, and the process
|
||||||
|
will exit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def wrapper():
|
||||||
|
try:
|
||||||
|
await cb(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
# previously, we used Failure().printTraceback() here, in the hope that
|
||||||
|
# would give better tracebacks than traceback.print_exc(). However, that
|
||||||
|
# doesn't handle chained exceptions (with a __cause__ or __context__) well,
|
||||||
|
# and I *think* the need for Failure() is reduced now that we mostly use
|
||||||
|
# async/await.
|
||||||
|
|
||||||
|
# Write the exception to both the logs *and* the unredirected stderr,
|
||||||
|
# because people tend to get confused if it only goes to one or the other.
|
||||||
|
#
|
||||||
|
# One problem with this is that if people are using a logging config that
|
||||||
|
# logs to the console (as is common eg under docker), they will get two
|
||||||
|
# copies of the exception. We could maybe try to detect that, but it's
|
||||||
|
# probably a cost we can bear.
|
||||||
|
logger.fatal("Error during startup", exc_info=True)
|
||||||
|
print("Error during startup:", file=sys.__stderr__)
|
||||||
|
traceback.print_exc(file=sys.__stderr__)
|
||||||
|
|
||||||
|
# it's no use calling sys.exit here, since that just raises a SystemExit
|
||||||
|
# exception which is then caught by the reactor, and everything carries
|
||||||
|
# on as normal.
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
reactor.callWhenRunning(lambda: defer.ensureDeferred(wrapper()))
|
||||||
|
|
||||||
|
|
||||||
def listen_metrics(bind_addresses, port):
|
def listen_metrics(bind_addresses, port):
|
||||||
"""
|
"""
|
||||||
Start Prometheus metrics server.
|
Start Prometheus metrics server.
|
||||||
@ -227,7 +267,7 @@ def refresh_certificate(hs):
|
|||||||
logger.info("Context factories updated.")
|
logger.info("Context factories updated.")
|
||||||
|
|
||||||
|
|
||||||
def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerConfig]):
|
async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerConfig]):
|
||||||
"""
|
"""
|
||||||
Start a Synapse server or worker.
|
Start a Synapse server or worker.
|
||||||
|
|
||||||
@ -241,75 +281,67 @@ def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerConfig]):
|
|||||||
hs: homeserver instance
|
hs: homeserver instance
|
||||||
listeners: Listener configuration ('listeners' in homeserver.yaml)
|
listeners: Listener configuration ('listeners' in homeserver.yaml)
|
||||||
"""
|
"""
|
||||||
try:
|
# Set up the SIGHUP machinery.
|
||||||
# Set up the SIGHUP machinery.
|
if hasattr(signal, "SIGHUP"):
|
||||||
if hasattr(signal, "SIGHUP"):
|
|
||||||
|
|
||||||
reactor = hs.get_reactor()
|
|
||||||
|
|
||||||
@wrap_as_background_process("sighup")
|
|
||||||
def handle_sighup(*args, **kwargs):
|
|
||||||
# Tell systemd our state, if we're using it. This will silently fail if
|
|
||||||
# we're not using systemd.
|
|
||||||
sdnotify(b"RELOADING=1")
|
|
||||||
|
|
||||||
for i, args, kwargs in _sighup_callbacks:
|
|
||||||
i(*args, **kwargs)
|
|
||||||
|
|
||||||
sdnotify(b"READY=1")
|
|
||||||
|
|
||||||
# We defer running the sighup handlers until next reactor tick. This
|
|
||||||
# is so that we're in a sane state, e.g. flushing the logs may fail
|
|
||||||
# if the sighup happens in the middle of writing a log entry.
|
|
||||||
def run_sighup(*args, **kwargs):
|
|
||||||
# `callFromThread` should be "signal safe" as well as thread
|
|
||||||
# safe.
|
|
||||||
reactor.callFromThread(handle_sighup, *args, **kwargs)
|
|
||||||
|
|
||||||
signal.signal(signal.SIGHUP, run_sighup)
|
|
||||||
|
|
||||||
register_sighup(refresh_certificate, hs)
|
|
||||||
|
|
||||||
# Load the certificate from disk.
|
|
||||||
refresh_certificate(hs)
|
|
||||||
|
|
||||||
# Start the tracer
|
|
||||||
synapse.logging.opentracing.init_tracer( # type: ignore[attr-defined] # noqa
|
|
||||||
hs
|
|
||||||
)
|
|
||||||
|
|
||||||
# It is now safe to start your Synapse.
|
|
||||||
hs.start_listening(listeners)
|
|
||||||
hs.get_datastore().db_pool.start_profiling()
|
|
||||||
hs.get_pusherpool().start()
|
|
||||||
|
|
||||||
# Log when we start the shut down process.
|
|
||||||
hs.get_reactor().addSystemEventTrigger(
|
|
||||||
"before", "shutdown", logger.info, "Shutting down..."
|
|
||||||
)
|
|
||||||
|
|
||||||
setup_sentry(hs)
|
|
||||||
setup_sdnotify(hs)
|
|
||||||
|
|
||||||
# If background tasks are running on the main process, start collecting the
|
|
||||||
# phone home stats.
|
|
||||||
if hs.config.run_background_tasks:
|
|
||||||
start_phone_stats_home(hs)
|
|
||||||
|
|
||||||
# We now freeze all allocated objects in the hopes that (almost)
|
|
||||||
# everything currently allocated are things that will be used for the
|
|
||||||
# rest of time. Doing so means less work each GC (hopefully).
|
|
||||||
#
|
|
||||||
# This only works on Python 3.7
|
|
||||||
if sys.version_info >= (3, 7):
|
|
||||||
gc.collect()
|
|
||||||
gc.freeze()
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
reactor = hs.get_reactor()
|
reactor = hs.get_reactor()
|
||||||
if reactor.running:
|
|
||||||
reactor.stop()
|
@wrap_as_background_process("sighup")
|
||||||
sys.exit(1)
|
def handle_sighup(*args, **kwargs):
|
||||||
|
# Tell systemd our state, if we're using it. This will silently fail if
|
||||||
|
# we're not using systemd.
|
||||||
|
sdnotify(b"RELOADING=1")
|
||||||
|
|
||||||
|
for i, args, kwargs in _sighup_callbacks:
|
||||||
|
i(*args, **kwargs)
|
||||||
|
|
||||||
|
sdnotify(b"READY=1")
|
||||||
|
|
||||||
|
# We defer running the sighup handlers until next reactor tick. This
|
||||||
|
# is so that we're in a sane state, e.g. flushing the logs may fail
|
||||||
|
# if the sighup happens in the middle of writing a log entry.
|
||||||
|
def run_sighup(*args, **kwargs):
|
||||||
|
# `callFromThread` should be "signal safe" as well as thread
|
||||||
|
# safe.
|
||||||
|
reactor.callFromThread(handle_sighup, *args, **kwargs)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGHUP, run_sighup)
|
||||||
|
|
||||||
|
register_sighup(refresh_certificate, hs)
|
||||||
|
|
||||||
|
# Load the certificate from disk.
|
||||||
|
refresh_certificate(hs)
|
||||||
|
|
||||||
|
# Start the tracer
|
||||||
|
synapse.logging.opentracing.init_tracer( # type: ignore[attr-defined] # noqa
|
||||||
|
hs
|
||||||
|
)
|
||||||
|
|
||||||
|
# It is now safe to start your Synapse.
|
||||||
|
hs.start_listening(listeners)
|
||||||
|
hs.get_datastore().db_pool.start_profiling()
|
||||||
|
hs.get_pusherpool().start()
|
||||||
|
|
||||||
|
# Log when we start the shut down process.
|
||||||
|
hs.get_reactor().addSystemEventTrigger(
|
||||||
|
"before", "shutdown", logger.info, "Shutting down..."
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_sentry(hs)
|
||||||
|
setup_sdnotify(hs)
|
||||||
|
|
||||||
|
# If background tasks are running on the main process, start collecting the
|
||||||
|
# phone home stats.
|
||||||
|
if hs.config.run_background_tasks:
|
||||||
|
start_phone_stats_home(hs)
|
||||||
|
|
||||||
|
# We now freeze all allocated objects in the hopes that (almost)
|
||||||
|
# everything currently allocated are things that will be used for the
|
||||||
|
# rest of time. Doing so means less work each GC (hopefully).
|
||||||
|
#
|
||||||
|
# This only works on Python 3.7
|
||||||
|
if sys.version_info >= (3, 7):
|
||||||
|
gc.collect()
|
||||||
|
gc.freeze()
|
||||||
|
|
||||||
|
|
||||||
def setup_sentry(hs):
|
def setup_sentry(hs):
|
||||||
|
@ -21,7 +21,7 @@ from typing import Dict, Iterable, Optional, Set
|
|||||||
|
|
||||||
from typing_extensions import ContextManager
|
from typing_extensions import ContextManager
|
||||||
|
|
||||||
from twisted.internet import address, reactor
|
from twisted.internet import address
|
||||||
|
|
||||||
import synapse
|
import synapse
|
||||||
import synapse.events
|
import synapse.events
|
||||||
@ -34,6 +34,7 @@ from synapse.api.urls import (
|
|||||||
SERVER_KEY_V2_PREFIX,
|
SERVER_KEY_V2_PREFIX,
|
||||||
)
|
)
|
||||||
from synapse.app import _base
|
from synapse.app import _base
|
||||||
|
from synapse.app._base import register_start
|
||||||
from synapse.config._base import ConfigError
|
from synapse.config._base import ConfigError
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.config.logger import setup_logging
|
from synapse.config.logger import setup_logging
|
||||||
@ -99,14 +100,28 @@ from synapse.rest.client.v1.profile import (
|
|||||||
)
|
)
|
||||||
from synapse.rest.client.v1.push_rule import PushRuleRestServlet
|
from synapse.rest.client.v1.push_rule import PushRuleRestServlet
|
||||||
from synapse.rest.client.v1.voip import VoipRestServlet
|
from synapse.rest.client.v1.voip import VoipRestServlet
|
||||||
from synapse.rest.client.v2_alpha import groups, sync, user_directory
|
from synapse.rest.client.v2_alpha import (
|
||||||
|
account_data,
|
||||||
|
groups,
|
||||||
|
read_marker,
|
||||||
|
receipts,
|
||||||
|
room_keys,
|
||||||
|
sync,
|
||||||
|
tags,
|
||||||
|
user_directory,
|
||||||
|
)
|
||||||
from synapse.rest.client.v2_alpha._base import client_patterns
|
from synapse.rest.client.v2_alpha._base import client_patterns
|
||||||
from synapse.rest.client.v2_alpha.account import ThreepidRestServlet
|
from synapse.rest.client.v2_alpha.account import ThreepidRestServlet
|
||||||
from synapse.rest.client.v2_alpha.account_data import (
|
from synapse.rest.client.v2_alpha.account_data import (
|
||||||
AccountDataServlet,
|
AccountDataServlet,
|
||||||
RoomAccountDataServlet,
|
RoomAccountDataServlet,
|
||||||
)
|
)
|
||||||
from synapse.rest.client.v2_alpha.keys import KeyChangesServlet, KeyQueryServlet
|
from synapse.rest.client.v2_alpha.devices import DevicesRestServlet
|
||||||
|
from synapse.rest.client.v2_alpha.keys import (
|
||||||
|
KeyChangesServlet,
|
||||||
|
KeyQueryServlet,
|
||||||
|
OneTimeKeyServlet,
|
||||||
|
)
|
||||||
from synapse.rest.client.v2_alpha.register import RegisterRestServlet
|
from synapse.rest.client.v2_alpha.register import RegisterRestServlet
|
||||||
from synapse.rest.client.v2_alpha.sendtodevice import SendToDeviceRestServlet
|
from synapse.rest.client.v2_alpha.sendtodevice import SendToDeviceRestServlet
|
||||||
from synapse.rest.client.versions import VersionsRestServlet
|
from synapse.rest.client.versions import VersionsRestServlet
|
||||||
@ -115,6 +130,7 @@ from synapse.rest.key.v2 import KeyApiV2Resource
|
|||||||
from synapse.server import HomeServer, cache_in_self
|
from synapse.server import HomeServer, cache_in_self
|
||||||
from synapse.storage.databases.main.censor_events import CensorEventsStore
|
from synapse.storage.databases.main.censor_events import CensorEventsStore
|
||||||
from synapse.storage.databases.main.client_ips import ClientIpWorkerStore
|
from synapse.storage.databases.main.client_ips import ClientIpWorkerStore
|
||||||
|
from synapse.storage.databases.main.e2e_room_keys import EndToEndRoomKeyStore
|
||||||
from synapse.storage.databases.main.media_repository import MediaRepositoryStore
|
from synapse.storage.databases.main.media_repository import MediaRepositoryStore
|
||||||
from synapse.storage.databases.main.metrics import ServerMetricsStore
|
from synapse.storage.databases.main.metrics import ServerMetricsStore
|
||||||
from synapse.storage.databases.main.monthly_active_users import (
|
from synapse.storage.databases.main.monthly_active_users import (
|
||||||
@ -446,6 +462,7 @@ class GenericWorkerSlavedStore(
|
|||||||
UserDirectoryStore,
|
UserDirectoryStore,
|
||||||
StatsStore,
|
StatsStore,
|
||||||
UIAuthWorkerStore,
|
UIAuthWorkerStore,
|
||||||
|
EndToEndRoomKeyStore,
|
||||||
SlavedDeviceInboxStore,
|
SlavedDeviceInboxStore,
|
||||||
SlavedDeviceStore,
|
SlavedDeviceStore,
|
||||||
SlavedReceiptsStore,
|
SlavedReceiptsStore,
|
||||||
@ -502,7 +519,9 @@ class GenericWorkerServer(HomeServer):
|
|||||||
RegisterRestServlet(self).register(resource)
|
RegisterRestServlet(self).register(resource)
|
||||||
LoginRestServlet(self).register(resource)
|
LoginRestServlet(self).register(resource)
|
||||||
ThreepidRestServlet(self).register(resource)
|
ThreepidRestServlet(self).register(resource)
|
||||||
|
DevicesRestServlet(self).register(resource)
|
||||||
KeyQueryServlet(self).register(resource)
|
KeyQueryServlet(self).register(resource)
|
||||||
|
OneTimeKeyServlet(self).register(resource)
|
||||||
KeyChangesServlet(self).register(resource)
|
KeyChangesServlet(self).register(resource)
|
||||||
VoipRestServlet(self).register(resource)
|
VoipRestServlet(self).register(resource)
|
||||||
PushRuleRestServlet(self).register(resource)
|
PushRuleRestServlet(self).register(resource)
|
||||||
@ -520,6 +539,11 @@ class GenericWorkerServer(HomeServer):
|
|||||||
room.register_servlets(self, resource, True)
|
room.register_servlets(self, resource, True)
|
||||||
room.register_deprecated_servlets(self, resource)
|
room.register_deprecated_servlets(self, resource)
|
||||||
InitialSyncRestServlet(self).register(resource)
|
InitialSyncRestServlet(self).register(resource)
|
||||||
|
room_keys.register_servlets(self, resource)
|
||||||
|
tags.register_servlets(self, resource)
|
||||||
|
account_data.register_servlets(self, resource)
|
||||||
|
receipts.register_servlets(self, resource)
|
||||||
|
read_marker.register_servlets(self, resource)
|
||||||
|
|
||||||
SendToDeviceRestServlet(self).register(resource)
|
SendToDeviceRestServlet(self).register(resource)
|
||||||
|
|
||||||
@ -960,9 +984,7 @@ def start(config_options):
|
|||||||
# streams. Will no-op if no streams can be written to by this worker.
|
# streams. Will no-op if no streams can be written to by this worker.
|
||||||
hs.get_replication_streamer()
|
hs.get_replication_streamer()
|
||||||
|
|
||||||
reactor.addSystemEventTrigger(
|
register_start(_base.start, hs, config.worker_listeners)
|
||||||
"before", "startup", _base.start, hs, config.worker_listeners
|
|
||||||
)
|
|
||||||
|
|
||||||
_base.start_worker_reactor("synapse-generic-worker", config)
|
_base.start_worker_reactor("synapse-generic-worker", config)
|
||||||
|
|
||||||
|
@ -15,15 +15,12 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import gc
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Iterable, Iterator
|
from typing import Iterable, Iterator
|
||||||
|
|
||||||
from twisted.application import service
|
from twisted.internet import reactor
|
||||||
from twisted.internet import defer, reactor
|
|
||||||
from twisted.python.failure import Failure
|
|
||||||
from twisted.web.resource import EncodingResourceWrapper, IResource
|
from twisted.web.resource import EncodingResourceWrapper, IResource
|
||||||
from twisted.web.server import GzipEncoderFactory
|
from twisted.web.server import GzipEncoderFactory
|
||||||
from twisted.web.static import File
|
from twisted.web.static import File
|
||||||
@ -40,7 +37,7 @@ from synapse.api.urls import (
|
|||||||
WEB_CLIENT_PREFIX,
|
WEB_CLIENT_PREFIX,
|
||||||
)
|
)
|
||||||
from synapse.app import _base
|
from synapse.app import _base
|
||||||
from synapse.app._base import listen_ssl, listen_tcp, quit_with_error
|
from synapse.app._base import listen_ssl, listen_tcp, quit_with_error, register_start
|
||||||
from synapse.config._base import ConfigError
|
from synapse.config._base import ConfigError
|
||||||
from synapse.config.emailconfig import ThreepidBehaviour
|
from synapse.config.emailconfig import ThreepidBehaviour
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
@ -73,7 +70,6 @@ from synapse.storage.prepare_database import UpgradeDatabaseException
|
|||||||
from synapse.util.httpresourcetree import create_resource_tree
|
from synapse.util.httpresourcetree import create_resource_tree
|
||||||
from synapse.util.manhole import manhole
|
from synapse.util.manhole import manhole
|
||||||
from synapse.util.module_loader import load_module
|
from synapse.util.module_loader import load_module
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
from synapse.util.versionstring import get_version_string
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.homeserver")
|
logger = logging.getLogger("synapse.app.homeserver")
|
||||||
@ -417,40 +413,28 @@ def setup(config_options):
|
|||||||
_base.refresh_certificate(hs)
|
_base.refresh_certificate(hs)
|
||||||
|
|
||||||
async def start():
|
async def start():
|
||||||
try:
|
# Run the ACME provisioning code, if it's enabled.
|
||||||
# Run the ACME provisioning code, if it's enabled.
|
if hs.config.acme_enabled:
|
||||||
if hs.config.acme_enabled:
|
acme = hs.get_acme_handler()
|
||||||
acme = hs.get_acme_handler()
|
# Start up the webservices which we will respond to ACME
|
||||||
# Start up the webservices which we will respond to ACME
|
# challenges with, and then provision.
|
||||||
# challenges with, and then provision.
|
await acme.start_listening()
|
||||||
await acme.start_listening()
|
await do_acme()
|
||||||
await do_acme()
|
|
||||||
|
|
||||||
# Check if it needs to be reprovisioned every day.
|
# Check if it needs to be reprovisioned every day.
|
||||||
hs.get_clock().looping_call(reprovision_acme, 24 * 60 * 60 * 1000)
|
hs.get_clock().looping_call(reprovision_acme, 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
# Load the OIDC provider metadatas, if OIDC is enabled.
|
# Load the OIDC provider metadatas, if OIDC is enabled.
|
||||||
if hs.config.oidc_enabled:
|
if hs.config.oidc_enabled:
|
||||||
oidc = hs.get_oidc_handler()
|
oidc = hs.get_oidc_handler()
|
||||||
# Loading the provider metadata also ensures the provider config is valid.
|
# Loading the provider metadata also ensures the provider config is valid.
|
||||||
await oidc.load_metadata()
|
await oidc.load_metadata()
|
||||||
await oidc.load_jwks()
|
|
||||||
|
|
||||||
_base.start(hs, config.listeners)
|
await _base.start(hs, config.listeners)
|
||||||
|
|
||||||
hs.get_datastore().db_pool.updates.start_doing_background_updates()
|
hs.get_datastore().db_pool.updates.start_doing_background_updates()
|
||||||
except Exception:
|
|
||||||
# Print the exception and bail out.
|
|
||||||
print("Error during startup:", file=sys.stderr)
|
|
||||||
|
|
||||||
# this gives better tracebacks than traceback.print_exc()
|
register_start(start)
|
||||||
Failure().printTraceback(file=sys.stderr)
|
|
||||||
|
|
||||||
if reactor.running:
|
|
||||||
reactor.stop()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
reactor.callWhenRunning(lambda: defer.ensureDeferred(start()))
|
|
||||||
|
|
||||||
return hs
|
return hs
|
||||||
|
|
||||||
@ -487,25 +471,6 @@ def format_config_error(e: ConfigError) -> Iterator[str]:
|
|||||||
e = e.__cause__
|
e = e.__cause__
|
||||||
|
|
||||||
|
|
||||||
class SynapseService(service.Service):
|
|
||||||
"""
|
|
||||||
A twisted Service class that will start synapse. Used to run synapse
|
|
||||||
via twistd and a .tac.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def startService(self):
|
|
||||||
hs = setup(self.config)
|
|
||||||
change_resource_limit(hs.config.soft_file_limit)
|
|
||||||
if hs.config.gc_thresholds:
|
|
||||||
gc.set_threshold(*hs.config.gc_thresholds)
|
|
||||||
|
|
||||||
def stopService(self):
|
|
||||||
return self._port.stopListening()
|
|
||||||
|
|
||||||
|
|
||||||
def run(hs):
|
def run(hs):
|
||||||
PROFILE_SYNAPSE = False
|
PROFILE_SYNAPSE = False
|
||||||
if PROFILE_SYNAPSE:
|
if PROFILE_SYNAPSE:
|
||||||
|
@ -252,11 +252,12 @@ class Config:
|
|||||||
env = jinja2.Environment(loader=loader, autoescape=autoescape)
|
env = jinja2.Environment(loader=loader, autoescape=autoescape)
|
||||||
|
|
||||||
# Update the environment with our custom filters
|
# Update the environment with our custom filters
|
||||||
env.filters.update({"format_ts": _format_ts_filter})
|
env.filters.update(
|
||||||
if self.public_baseurl:
|
{
|
||||||
env.filters.update(
|
"format_ts": _format_ts_filter,
|
||||||
{"mxc_to_http": _create_mxc_to_http_filter(self.public_baseurl)}
|
"mxc_to_http": _create_mxc_to_http_filter(self.public_baseurl),
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
# Load the template
|
# Load the template
|
||||||
|
@ -40,7 +40,7 @@ class CasConfig(Config):
|
|||||||
self.cas_required_attributes = {}
|
self.cas_required_attributes = {}
|
||||||
|
|
||||||
def generate_config_section(self, config_dir_path, server_name, **kwargs):
|
def generate_config_section(self, config_dir_path, server_name, **kwargs):
|
||||||
return """
|
return """\
|
||||||
# Enable Central Authentication Service (CAS) for registration and login.
|
# Enable Central Authentication Service (CAS) for registration and login.
|
||||||
#
|
#
|
||||||
cas_config:
|
cas_config:
|
||||||
|
@ -166,11 +166,6 @@ class EmailConfig(Config):
|
|||||||
if not self.email_notif_from:
|
if not self.email_notif_from:
|
||||||
missing.append("email.notif_from")
|
missing.append("email.notif_from")
|
||||||
|
|
||||||
# public_baseurl is required to build password reset and validation links that
|
|
||||||
# will be emailed to users
|
|
||||||
if config.get("public_baseurl") is None:
|
|
||||||
missing.append("public_baseurl")
|
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
MISSING_PASSWORD_RESET_CONFIG_ERROR % (", ".join(missing),)
|
MISSING_PASSWORD_RESET_CONFIG_ERROR % (", ".join(missing),)
|
||||||
@ -269,9 +264,6 @@ class EmailConfig(Config):
|
|||||||
if not self.email_notif_from:
|
if not self.email_notif_from:
|
||||||
missing.append("email.notif_from")
|
missing.append("email.notif_from")
|
||||||
|
|
||||||
if config.get("public_baseurl") is None:
|
|
||||||
missing.append("public_baseurl")
|
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"email.enable_notifs is True but required keys are missing: %s"
|
"email.enable_notifs is True but required keys are missing: %s"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2020 Quentin Gliech
|
# Copyright 2020 Quentin Gliech
|
||||||
|
# Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -13,8 +14,17 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import string
|
||||||
|
from collections import Counter
|
||||||
|
from typing import Iterable, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
from synapse.config._util import validate_config
|
||||||
from synapse.python_dependencies import DependencyException, check_requirements
|
from synapse.python_dependencies import DependencyException, check_requirements
|
||||||
|
from synapse.types import Collection, JsonDict
|
||||||
from synapse.util.module_loader import load_module
|
from synapse.util.module_loader import load_module
|
||||||
|
from synapse.util.stringutils import parse_and_validate_mxc_uri
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
@ -25,202 +35,442 @@ class OIDCConfig(Config):
|
|||||||
section = "oidc"
|
section = "oidc"
|
||||||
|
|
||||||
def read_config(self, config, **kwargs):
|
def read_config(self, config, **kwargs):
|
||||||
self.oidc_enabled = False
|
self.oidc_providers = tuple(_parse_oidc_provider_configs(config))
|
||||||
|
if not self.oidc_providers:
|
||||||
oidc_config = config.get("oidc_config")
|
|
||||||
|
|
||||||
if not oidc_config or not oidc_config.get("enabled", False):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_requirements("oidc")
|
check_requirements("oidc")
|
||||||
except DependencyException as e:
|
except DependencyException as e:
|
||||||
raise ConfigError(e.message)
|
raise ConfigError(e.message) from e
|
||||||
|
|
||||||
|
# check we don't have any duplicate idp_ids now. (The SSO handler will also
|
||||||
|
# check for duplicates when the REST listeners get registered, but that happens
|
||||||
|
# after synapse has forked so doesn't give nice errors.)
|
||||||
|
c = Counter([i.idp_id for i in self.oidc_providers])
|
||||||
|
for idp_id, count in c.items():
|
||||||
|
if count > 1:
|
||||||
|
raise ConfigError(
|
||||||
|
"Multiple OIDC providers have the idp_id %r." % idp_id
|
||||||
|
)
|
||||||
|
|
||||||
public_baseurl = self.public_baseurl
|
public_baseurl = self.public_baseurl
|
||||||
if public_baseurl is None:
|
|
||||||
raise ConfigError("oidc_config requires a public_baseurl to be set")
|
|
||||||
self.oidc_callback_url = public_baseurl + "_synapse/oidc/callback"
|
self.oidc_callback_url = public_baseurl + "_synapse/oidc/callback"
|
||||||
|
|
||||||
self.oidc_enabled = True
|
@property
|
||||||
self.oidc_discover = oidc_config.get("discover", True)
|
def oidc_enabled(self) -> bool:
|
||||||
self.oidc_issuer = oidc_config["issuer"]
|
# OIDC is enabled if we have a provider
|
||||||
self.oidc_client_id = oidc_config["client_id"]
|
return bool(self.oidc_providers)
|
||||||
self.oidc_client_secret = oidc_config["client_secret"]
|
|
||||||
self.oidc_client_auth_method = oidc_config.get(
|
|
||||||
"client_auth_method", "client_secret_basic"
|
|
||||||
)
|
|
||||||
self.oidc_scopes = oidc_config.get("scopes", ["openid"])
|
|
||||||
self.oidc_authorization_endpoint = oidc_config.get("authorization_endpoint")
|
|
||||||
self.oidc_token_endpoint = oidc_config.get("token_endpoint")
|
|
||||||
self.oidc_userinfo_endpoint = oidc_config.get("userinfo_endpoint")
|
|
||||||
self.oidc_jwks_uri = oidc_config.get("jwks_uri")
|
|
||||||
self.oidc_skip_verification = oidc_config.get("skip_verification", False)
|
|
||||||
self.oidc_user_profile_method = oidc_config.get("user_profile_method", "auto")
|
|
||||||
self.oidc_allow_existing_users = oidc_config.get("allow_existing_users", False)
|
|
||||||
|
|
||||||
ump_config = oidc_config.get("user_mapping_provider", {})
|
|
||||||
ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
|
|
||||||
ump_config.setdefault("config", {})
|
|
||||||
|
|
||||||
(
|
|
||||||
self.oidc_user_mapping_provider_class,
|
|
||||||
self.oidc_user_mapping_provider_config,
|
|
||||||
) = load_module(ump_config, ("oidc_config", "user_mapping_provider"))
|
|
||||||
|
|
||||||
# Ensure loaded user mapping module has defined all necessary methods
|
|
||||||
required_methods = [
|
|
||||||
"get_remote_user_id",
|
|
||||||
"map_user_attributes",
|
|
||||||
]
|
|
||||||
missing_methods = [
|
|
||||||
method
|
|
||||||
for method in required_methods
|
|
||||||
if not hasattr(self.oidc_user_mapping_provider_class, method)
|
|
||||||
]
|
|
||||||
if missing_methods:
|
|
||||||
raise ConfigError(
|
|
||||||
"Class specified by oidc_config."
|
|
||||||
"user_mapping_provider.module is missing required "
|
|
||||||
"methods: %s" % (", ".join(missing_methods),)
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_config_section(self, config_dir_path, server_name, **kwargs):
|
def generate_config_section(self, config_dir_path, server_name, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
# Enable OpenID Connect (OIDC) / OAuth 2.0 for registration and login.
|
# List of OpenID Connect (OIDC) / OAuth 2.0 identity providers, for registration
|
||||||
|
# and login.
|
||||||
|
#
|
||||||
|
# Options for each entry include:
|
||||||
|
#
|
||||||
|
# idp_id: a unique identifier for this identity provider. Used internally
|
||||||
|
# by Synapse; should be a single word such as 'github'.
|
||||||
|
#
|
||||||
|
# Note that, if this is changed, users authenticating via that provider
|
||||||
|
# will no longer be recognised as the same user!
|
||||||
|
#
|
||||||
|
# idp_name: A user-facing name for this identity provider, which is used to
|
||||||
|
# offer the user a choice of login mechanisms.
|
||||||
|
#
|
||||||
|
# idp_icon: An optional icon for this identity provider, which is presented
|
||||||
|
# by identity picker pages. If given, must be an MXC URI of the format
|
||||||
|
# mxc://<server-name>/<media-id>. (An easy way to obtain such an MXC URI
|
||||||
|
# is to upload an image to an (unencrypted) room and then copy the "url"
|
||||||
|
# from the source of the event.)
|
||||||
|
#
|
||||||
|
# discover: set to 'false' to disable the use of the OIDC discovery mechanism
|
||||||
|
# to discover endpoints. Defaults to true.
|
||||||
|
#
|
||||||
|
# issuer: Required. The OIDC issuer. Used to validate tokens and (if discovery
|
||||||
|
# is enabled) to discover the provider's endpoints.
|
||||||
|
#
|
||||||
|
# client_id: Required. oauth2 client id to use.
|
||||||
|
#
|
||||||
|
# client_secret: Required. oauth2 client secret to use.
|
||||||
|
#
|
||||||
|
# client_auth_method: auth method to use when exchanging the token. Valid
|
||||||
|
# values are 'client_secret_basic' (default), 'client_secret_post' and
|
||||||
|
# 'none'.
|
||||||
|
#
|
||||||
|
# scopes: list of scopes to request. This should normally include the "openid"
|
||||||
|
# scope. Defaults to ["openid"].
|
||||||
|
#
|
||||||
|
# authorization_endpoint: the oauth2 authorization endpoint. Required if
|
||||||
|
# provider discovery is disabled.
|
||||||
|
#
|
||||||
|
# token_endpoint: the oauth2 token endpoint. Required if provider discovery is
|
||||||
|
# disabled.
|
||||||
|
#
|
||||||
|
# userinfo_endpoint: the OIDC userinfo endpoint. Required if discovery is
|
||||||
|
# disabled and the 'openid' scope is not requested.
|
||||||
|
#
|
||||||
|
# jwks_uri: URI where to fetch the JWKS. Required if discovery is disabled and
|
||||||
|
# the 'openid' scope is used.
|
||||||
|
#
|
||||||
|
# skip_verification: set to 'true' to skip metadata verification. Use this if
|
||||||
|
# you are connecting to a provider that is not OpenID Connect compliant.
|
||||||
|
# Defaults to false. Avoid this in production.
|
||||||
|
#
|
||||||
|
# user_profile_method: Whether to fetch the user profile from the userinfo
|
||||||
|
# endpoint. Valid values are: 'auto' or 'userinfo_endpoint'.
|
||||||
|
#
|
||||||
|
# Defaults to 'auto', which fetches the userinfo endpoint if 'openid' is
|
||||||
|
# included in 'scopes'. Set to 'userinfo_endpoint' to always fetch the
|
||||||
|
# userinfo endpoint.
|
||||||
|
#
|
||||||
|
# allow_existing_users: set to 'true' to allow a user logging in via OIDC to
|
||||||
|
# match a pre-existing account instead of failing. This could be used if
|
||||||
|
# switching from password logins to OIDC. Defaults to false.
|
||||||
|
#
|
||||||
|
# user_mapping_provider: Configuration for how attributes returned from a OIDC
|
||||||
|
# provider are mapped onto a matrix user. This setting has the following
|
||||||
|
# sub-properties:
|
||||||
|
#
|
||||||
|
# module: The class name of a custom mapping module. Default is
|
||||||
|
# {mapping_provider!r}.
|
||||||
|
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
|
||||||
|
# for information on implementing a custom mapping provider.
|
||||||
|
#
|
||||||
|
# config: Configuration for the mapping provider module. This section will
|
||||||
|
# be passed as a Python dictionary to the user mapping provider
|
||||||
|
# module's `parse_config` method.
|
||||||
|
#
|
||||||
|
# For the default provider, the following settings are available:
|
||||||
|
#
|
||||||
|
# sub: name of the claim containing a unique identifier for the
|
||||||
|
# user. Defaults to 'sub', which OpenID Connect compliant
|
||||||
|
# providers should provide.
|
||||||
|
#
|
||||||
|
# localpart_template: Jinja2 template for the localpart of the MXID.
|
||||||
|
# If this is not set, the user will be prompted to choose their
|
||||||
|
# own username.
|
||||||
|
#
|
||||||
|
# display_name_template: Jinja2 template for the display name to set
|
||||||
|
# on first login. If unset, no displayname will be set.
|
||||||
|
#
|
||||||
|
# extra_attributes: a map of Jinja2 templates for extra attributes
|
||||||
|
# to send back to the client during login.
|
||||||
|
# Note that these are non-standard and clients will ignore them
|
||||||
|
# without modifications.
|
||||||
|
#
|
||||||
|
# When rendering, the Jinja2 templates are given a 'user' variable,
|
||||||
|
# which is set to the claims returned by the UserInfo Endpoint and/or
|
||||||
|
# in the ID Token.
|
||||||
#
|
#
|
||||||
# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
|
# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
|
||||||
# for some example configurations.
|
# for information on how to configure these options.
|
||||||
#
|
#
|
||||||
oidc_config:
|
# For backwards compatibility, it is also possible to configure a single OIDC
|
||||||
# Uncomment the following to enable authorization against an OpenID Connect
|
# provider via an 'oidc_config' setting. This is now deprecated and admins are
|
||||||
# server. Defaults to false.
|
# advised to migrate to the 'oidc_providers' format. (When doing that migration,
|
||||||
|
# use 'oidc' for the idp_id to ensure that existing users continue to be
|
||||||
|
# recognised.)
|
||||||
|
#
|
||||||
|
oidc_providers:
|
||||||
|
# Generic example
|
||||||
#
|
#
|
||||||
#enabled: true
|
#- idp_id: my_idp
|
||||||
|
# idp_name: "My OpenID provider"
|
||||||
|
# idp_icon: "mxc://example.com/mediaid"
|
||||||
|
# discover: false
|
||||||
|
# issuer: "https://accounts.example.com/"
|
||||||
|
# client_id: "provided-by-your-issuer"
|
||||||
|
# client_secret: "provided-by-your-issuer"
|
||||||
|
# client_auth_method: client_secret_post
|
||||||
|
# scopes: ["openid", "profile"]
|
||||||
|
# authorization_endpoint: "https://accounts.example.com/oauth2/auth"
|
||||||
|
# token_endpoint: "https://accounts.example.com/oauth2/token"
|
||||||
|
# userinfo_endpoint: "https://accounts.example.com/userinfo"
|
||||||
|
# jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
|
||||||
|
# skip_verification: true
|
||||||
|
|
||||||
# Uncomment the following to disable use of the OIDC discovery mechanism to
|
# For use with Keycloak
|
||||||
# discover endpoints. Defaults to true.
|
|
||||||
#
|
#
|
||||||
#discover: false
|
#- idp_id: keycloak
|
||||||
|
# idp_name: Keycloak
|
||||||
|
# issuer: "https://127.0.0.1:8443/auth/realms/my_realm_name"
|
||||||
|
# client_id: "synapse"
|
||||||
|
# client_secret: "copy secret generated in Keycloak UI"
|
||||||
|
# scopes: ["openid", "profile"]
|
||||||
|
|
||||||
# the OIDC issuer. Used to validate tokens and (if discovery is enabled) to
|
# For use with Github
|
||||||
# discover the provider's endpoints.
|
|
||||||
#
|
#
|
||||||
# Required if 'enabled' is true.
|
#- idp_id: github
|
||||||
#
|
# idp_name: Github
|
||||||
#issuer: "https://accounts.example.com/"
|
# discover: false
|
||||||
|
# issuer: "https://github.com/"
|
||||||
# oauth2 client id to use.
|
# client_id: "your-client-id" # TO BE FILLED
|
||||||
#
|
# client_secret: "your-client-secret" # TO BE FILLED
|
||||||
# Required if 'enabled' is true.
|
# authorization_endpoint: "https://github.com/login/oauth/authorize"
|
||||||
#
|
# token_endpoint: "https://github.com/login/oauth/access_token"
|
||||||
#client_id: "provided-by-your-issuer"
|
# userinfo_endpoint: "https://api.github.com/user"
|
||||||
|
# scopes: ["read:user"]
|
||||||
# oauth2 client secret to use.
|
# user_mapping_provider:
|
||||||
#
|
# config:
|
||||||
# Required if 'enabled' is true.
|
# subject_claim: "id"
|
||||||
#
|
# localpart_template: "{{ user.login }}"
|
||||||
#client_secret: "provided-by-your-issuer"
|
# display_name_template: "{{ user.name }}"
|
||||||
|
|
||||||
# auth method to use when exchanging the token.
|
|
||||||
# Valid values are 'client_secret_basic' (default), 'client_secret_post' and
|
|
||||||
# 'none'.
|
|
||||||
#
|
|
||||||
#client_auth_method: client_secret_post
|
|
||||||
|
|
||||||
# list of scopes to request. This should normally include the "openid" scope.
|
|
||||||
# Defaults to ["openid"].
|
|
||||||
#
|
|
||||||
#scopes: ["openid", "profile"]
|
|
||||||
|
|
||||||
# the oauth2 authorization endpoint. Required if provider discovery is disabled.
|
|
||||||
#
|
|
||||||
#authorization_endpoint: "https://accounts.example.com/oauth2/auth"
|
|
||||||
|
|
||||||
# the oauth2 token endpoint. Required if provider discovery is disabled.
|
|
||||||
#
|
|
||||||
#token_endpoint: "https://accounts.example.com/oauth2/token"
|
|
||||||
|
|
||||||
# the OIDC userinfo endpoint. Required if discovery is disabled and the
|
|
||||||
# "openid" scope is not requested.
|
|
||||||
#
|
|
||||||
#userinfo_endpoint: "https://accounts.example.com/userinfo"
|
|
||||||
|
|
||||||
# URI where to fetch the JWKS. Required if discovery is disabled and the
|
|
||||||
# "openid" scope is used.
|
|
||||||
#
|
|
||||||
#jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
|
|
||||||
|
|
||||||
# Uncomment to skip metadata verification. Defaults to false.
|
|
||||||
#
|
|
||||||
# Use this if you are connecting to a provider that is not OpenID Connect
|
|
||||||
# compliant.
|
|
||||||
# Avoid this in production.
|
|
||||||
#
|
|
||||||
#skip_verification: true
|
|
||||||
|
|
||||||
# Whether to fetch the user profile from the userinfo endpoint. Valid
|
|
||||||
# values are: "auto" or "userinfo_endpoint".
|
|
||||||
#
|
|
||||||
# Defaults to "auto", which fetches the userinfo endpoint if "openid" is included
|
|
||||||
# in `scopes`. Uncomment the following to always fetch the userinfo endpoint.
|
|
||||||
#
|
|
||||||
#user_profile_method: "userinfo_endpoint"
|
|
||||||
|
|
||||||
# Uncomment to allow a user logging in via OIDC to match a pre-existing account instead
|
|
||||||
# of failing. This could be used if switching from password logins to OIDC. Defaults to false.
|
|
||||||
#
|
|
||||||
#allow_existing_users: true
|
|
||||||
|
|
||||||
# An external module can be provided here as a custom solution to mapping
|
|
||||||
# attributes returned from a OIDC provider onto a matrix user.
|
|
||||||
#
|
|
||||||
user_mapping_provider:
|
|
||||||
# The custom module's class. Uncomment to use a custom module.
|
|
||||||
# Default is {mapping_provider!r}.
|
|
||||||
#
|
|
||||||
# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
|
|
||||||
# for information on implementing a custom mapping provider.
|
|
||||||
#
|
|
||||||
#module: mapping_provider.OidcMappingProvider
|
|
||||||
|
|
||||||
# Custom configuration values for the module. This section will be passed as
|
|
||||||
# a Python dictionary to the user mapping provider module's `parse_config`
|
|
||||||
# method.
|
|
||||||
#
|
|
||||||
# The examples below are intended for the default provider: they should be
|
|
||||||
# changed if using a custom provider.
|
|
||||||
#
|
|
||||||
config:
|
|
||||||
# name of the claim containing a unique identifier for the user.
|
|
||||||
# Defaults to `sub`, which OpenID Connect compliant providers should provide.
|
|
||||||
#
|
|
||||||
#subject_claim: "sub"
|
|
||||||
|
|
||||||
# Jinja2 template for the localpart of the MXID.
|
|
||||||
#
|
|
||||||
# When rendering, this template is given the following variables:
|
|
||||||
# * user: The claims returned by the UserInfo Endpoint and/or in the ID
|
|
||||||
# Token
|
|
||||||
#
|
|
||||||
# If this is not set, the user will be prompted to choose their
|
|
||||||
# own username.
|
|
||||||
#
|
|
||||||
#localpart_template: "{{{{ user.preferred_username }}}}"
|
|
||||||
|
|
||||||
# Jinja2 template for the display name to set on first login.
|
|
||||||
#
|
|
||||||
# If unset, no displayname will be set.
|
|
||||||
#
|
|
||||||
#display_name_template: "{{{{ user.given_name }}}} {{{{ user.last_name }}}}"
|
|
||||||
|
|
||||||
# Jinja2 templates for extra attributes to send back to the client during
|
|
||||||
# login.
|
|
||||||
#
|
|
||||||
# Note that these are non-standard and clients will ignore them without modifications.
|
|
||||||
#
|
|
||||||
#extra_attributes:
|
|
||||||
#birthdate: "{{{{ user.birthdate }}}}"
|
|
||||||
""".format(
|
""".format(
|
||||||
mapping_provider=DEFAULT_USER_MAPPING_PROVIDER
|
mapping_provider=DEFAULT_USER_MAPPING_PROVIDER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# jsonschema definition of the configuration settings for an oidc identity provider
|
||||||
|
OIDC_PROVIDER_CONFIG_SCHEMA = {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["issuer", "client_id", "client_secret"],
|
||||||
|
"properties": {
|
||||||
|
# TODO: fix the maxLength here depending on what MSC2528 decides
|
||||||
|
# remember that we prefix the ID given here with `oidc-`
|
||||||
|
"idp_id": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||||
|
"idp_name": {"type": "string"},
|
||||||
|
"idp_icon": {"type": "string"},
|
||||||
|
"discover": {"type": "boolean"},
|
||||||
|
"issuer": {"type": "string"},
|
||||||
|
"client_id": {"type": "string"},
|
||||||
|
"client_secret": {"type": "string"},
|
||||||
|
"client_auth_method": {
|
||||||
|
"type": "string",
|
||||||
|
# the following list is the same as the keys of
|
||||||
|
# authlib.oauth2.auth.ClientAuth.DEFAULT_AUTH_METHODS. We inline it
|
||||||
|
# to avoid importing authlib here.
|
||||||
|
"enum": ["client_secret_basic", "client_secret_post", "none"],
|
||||||
|
},
|
||||||
|
"scopes": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"authorization_endpoint": {"type": "string"},
|
||||||
|
"token_endpoint": {"type": "string"},
|
||||||
|
"userinfo_endpoint": {"type": "string"},
|
||||||
|
"jwks_uri": {"type": "string"},
|
||||||
|
"skip_verification": {"type": "boolean"},
|
||||||
|
"user_profile_method": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["auto", "userinfo_endpoint"],
|
||||||
|
},
|
||||||
|
"allow_existing_users": {"type": "boolean"},
|
||||||
|
"user_mapping_provider": {"type": ["object", "null"]},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# the same as OIDC_PROVIDER_CONFIG_SCHEMA, but with compulsory idp_id and idp_name
|
||||||
|
OIDC_PROVIDER_CONFIG_WITH_ID_SCHEMA = {
|
||||||
|
"allOf": [OIDC_PROVIDER_CONFIG_SCHEMA, {"required": ["idp_id", "idp_name"]}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# the `oidc_providers` list can either be None (as it is in the default config), or
|
||||||
|
# a list of provider configs, each of which requires an explicit ID and name.
|
||||||
|
OIDC_PROVIDER_LIST_SCHEMA = {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "null"},
|
||||||
|
{"type": "array", "items": OIDC_PROVIDER_CONFIG_WITH_ID_SCHEMA},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# the `oidc_config` setting can either be None (which it used to be in the default
|
||||||
|
# config), or an object. If an object, it is ignored unless it has an "enabled: True"
|
||||||
|
# property.
|
||||||
|
#
|
||||||
|
# It's *possible* to represent this with jsonschema, but the resultant errors aren't
|
||||||
|
# particularly clear, so we just check for either an object or a null here, and do
|
||||||
|
# additional checks in the code.
|
||||||
|
OIDC_CONFIG_SCHEMA = {"oneOf": [{"type": "null"}, {"type": "object"}]}
|
||||||
|
|
||||||
|
# the top-level schema can contain an "oidc_config" and/or an "oidc_providers".
|
||||||
|
MAIN_CONFIG_SCHEMA = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"oidc_config": OIDC_CONFIG_SCHEMA,
|
||||||
|
"oidc_providers": OIDC_PROVIDER_LIST_SCHEMA,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_oidc_provider_configs(config: JsonDict) -> Iterable["OidcProviderConfig"]:
|
||||||
|
"""extract and parse the OIDC provider configs from the config dict
|
||||||
|
|
||||||
|
The configuration may contain either a single `oidc_config` object with an
|
||||||
|
`enabled: True` property, or a list of provider configurations under
|
||||||
|
`oidc_providers`, *or both*.
|
||||||
|
|
||||||
|
Returns a generator which yields the OidcProviderConfig objects
|
||||||
|
"""
|
||||||
|
validate_config(MAIN_CONFIG_SCHEMA, config, ())
|
||||||
|
|
||||||
|
for i, p in enumerate(config.get("oidc_providers") or []):
|
||||||
|
yield _parse_oidc_config_dict(p, ("oidc_providers", "<item %i>" % (i,)))
|
||||||
|
|
||||||
|
# for backwards-compatibility, it is also possible to provide a single "oidc_config"
|
||||||
|
# object with an "enabled: True" property.
|
||||||
|
oidc_config = config.get("oidc_config")
|
||||||
|
if oidc_config and oidc_config.get("enabled", False):
|
||||||
|
# MAIN_CONFIG_SCHEMA checks that `oidc_config` is an object, but not that
|
||||||
|
# it matches OIDC_PROVIDER_CONFIG_SCHEMA (see the comments on OIDC_CONFIG_SCHEMA
|
||||||
|
# above), so now we need to validate it.
|
||||||
|
validate_config(OIDC_PROVIDER_CONFIG_SCHEMA, oidc_config, ("oidc_config",))
|
||||||
|
yield _parse_oidc_config_dict(oidc_config, ("oidc_config",))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_oidc_config_dict(
|
||||||
|
oidc_config: JsonDict, config_path: Tuple[str, ...]
|
||||||
|
) -> "OidcProviderConfig":
|
||||||
|
"""Take the configuration dict and parse it into an OidcProviderConfig
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConfigError if the configuration is malformed.
|
||||||
|
"""
|
||||||
|
ump_config = oidc_config.get("user_mapping_provider", {})
|
||||||
|
ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
|
||||||
|
ump_config.setdefault("config", {})
|
||||||
|
|
||||||
|
(user_mapping_provider_class, user_mapping_provider_config,) = load_module(
|
||||||
|
ump_config, config_path + ("user_mapping_provider",)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure loaded user mapping module has defined all necessary methods
|
||||||
|
required_methods = [
|
||||||
|
"get_remote_user_id",
|
||||||
|
"map_user_attributes",
|
||||||
|
]
|
||||||
|
missing_methods = [
|
||||||
|
method
|
||||||
|
for method in required_methods
|
||||||
|
if not hasattr(user_mapping_provider_class, method)
|
||||||
|
]
|
||||||
|
if missing_methods:
|
||||||
|
raise ConfigError(
|
||||||
|
"Class %s is missing required "
|
||||||
|
"methods: %s" % (user_mapping_provider_class, ", ".join(missing_methods),),
|
||||||
|
config_path + ("user_mapping_provider", "module"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# MSC2858 will apply certain limits in what can be used as an IdP id, so let's
|
||||||
|
# enforce those limits now.
|
||||||
|
# TODO: factor out this stuff to a generic function
|
||||||
|
idp_id = oidc_config.get("idp_id", "oidc")
|
||||||
|
|
||||||
|
# TODO: update this validity check based on what MSC2858 decides.
|
||||||
|
valid_idp_chars = set(string.ascii_lowercase + string.digits + "-._")
|
||||||
|
|
||||||
|
if any(c not in valid_idp_chars for c in idp_id):
|
||||||
|
raise ConfigError(
|
||||||
|
'idp_id may only contain a-z, 0-9, "-", ".", "_"',
|
||||||
|
config_path + ("idp_id",),
|
||||||
|
)
|
||||||
|
|
||||||
|
if idp_id[0] not in string.ascii_lowercase:
|
||||||
|
raise ConfigError(
|
||||||
|
"idp_id must start with a-z", config_path + ("idp_id",),
|
||||||
|
)
|
||||||
|
|
||||||
|
# prefix the given IDP with a prefix specific to the SSO mechanism, to avoid
|
||||||
|
# clashes with other mechs (such as SAML, CAS).
|
||||||
|
#
|
||||||
|
# We allow "oidc" as an exception so that people migrating from old-style
|
||||||
|
# "oidc_config" format (which has long used "oidc" as its idp_id) can migrate to
|
||||||
|
# a new-style "oidc_providers" entry without changing the idp_id for their provider
|
||||||
|
# (and thereby invalidating their user_external_ids data).
|
||||||
|
|
||||||
|
if idp_id != "oidc":
|
||||||
|
idp_id = "oidc-" + idp_id
|
||||||
|
|
||||||
|
# MSC2858 also specifies that the idp_icon must be a valid MXC uri
|
||||||
|
idp_icon = oidc_config.get("idp_icon")
|
||||||
|
if idp_icon is not None:
|
||||||
|
try:
|
||||||
|
parse_and_validate_mxc_uri(idp_icon)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ConfigError(
|
||||||
|
"idp_icon must be a valid MXC URI", config_path + ("idp_icon",)
|
||||||
|
) from e
|
||||||
|
|
||||||
|
return OidcProviderConfig(
|
||||||
|
idp_id=idp_id,
|
||||||
|
idp_name=oidc_config.get("idp_name", "OIDC"),
|
||||||
|
idp_icon=idp_icon,
|
||||||
|
discover=oidc_config.get("discover", True),
|
||||||
|
issuer=oidc_config["issuer"],
|
||||||
|
client_id=oidc_config["client_id"],
|
||||||
|
client_secret=oidc_config["client_secret"],
|
||||||
|
client_auth_method=oidc_config.get("client_auth_method", "client_secret_basic"),
|
||||||
|
scopes=oidc_config.get("scopes", ["openid"]),
|
||||||
|
authorization_endpoint=oidc_config.get("authorization_endpoint"),
|
||||||
|
token_endpoint=oidc_config.get("token_endpoint"),
|
||||||
|
userinfo_endpoint=oidc_config.get("userinfo_endpoint"),
|
||||||
|
jwks_uri=oidc_config.get("jwks_uri"),
|
||||||
|
skip_verification=oidc_config.get("skip_verification", False),
|
||||||
|
user_profile_method=oidc_config.get("user_profile_method", "auto"),
|
||||||
|
allow_existing_users=oidc_config.get("allow_existing_users", False),
|
||||||
|
user_mapping_provider_class=user_mapping_provider_class,
|
||||||
|
user_mapping_provider_config=user_mapping_provider_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True, frozen=True)
|
||||||
|
class OidcProviderConfig:
|
||||||
|
# a unique identifier for this identity provider. Used in the 'user_external_ids'
|
||||||
|
# table, as well as the query/path parameter used in the login protocol.
|
||||||
|
idp_id = attr.ib(type=str)
|
||||||
|
|
||||||
|
# user-facing name for this identity provider.
|
||||||
|
idp_name = attr.ib(type=str)
|
||||||
|
|
||||||
|
# Optional MXC URI for icon for this IdP.
|
||||||
|
idp_icon = attr.ib(type=Optional[str])
|
||||||
|
|
||||||
|
# whether the OIDC discovery mechanism is used to discover endpoints
|
||||||
|
discover = attr.ib(type=bool)
|
||||||
|
|
||||||
|
# the OIDC issuer. Used to validate tokens and (if discovery is enabled) to
|
||||||
|
# discover the provider's endpoints.
|
||||||
|
issuer = attr.ib(type=str)
|
||||||
|
|
||||||
|
# oauth2 client id to use
|
||||||
|
client_id = attr.ib(type=str)
|
||||||
|
|
||||||
|
# oauth2 client secret to use
|
||||||
|
client_secret = attr.ib(type=str)
|
||||||
|
|
||||||
|
# auth method to use when exchanging the token.
|
||||||
|
# Valid values are 'client_secret_basic', 'client_secret_post' and
|
||||||
|
# 'none'.
|
||||||
|
client_auth_method = attr.ib(type=str)
|
||||||
|
|
||||||
|
# list of scopes to request
|
||||||
|
scopes = attr.ib(type=Collection[str])
|
||||||
|
|
||||||
|
# the oauth2 authorization endpoint. Required if discovery is disabled.
|
||||||
|
authorization_endpoint = attr.ib(type=Optional[str])
|
||||||
|
|
||||||
|
# the oauth2 token endpoint. Required if discovery is disabled.
|
||||||
|
token_endpoint = attr.ib(type=Optional[str])
|
||||||
|
|
||||||
|
# the OIDC userinfo endpoint. Required if discovery is disabled and the
|
||||||
|
# "openid" scope is not requested.
|
||||||
|
userinfo_endpoint = attr.ib(type=Optional[str])
|
||||||
|
|
||||||
|
# URI where to fetch the JWKS. Required if discovery is disabled and the
|
||||||
|
# "openid" scope is used.
|
||||||
|
jwks_uri = attr.ib(type=Optional[str])
|
||||||
|
|
||||||
|
# Whether to skip metadata verification
|
||||||
|
skip_verification = attr.ib(type=bool)
|
||||||
|
|
||||||
|
# Whether to fetch the user profile from the userinfo endpoint. Valid
|
||||||
|
# values are: "auto" or "userinfo_endpoint".
|
||||||
|
user_profile_method = attr.ib(type=str)
|
||||||
|
|
||||||
|
# whether to allow a user logging in via OIDC to match a pre-existing account
|
||||||
|
# instead of failing
|
||||||
|
allow_existing_users = attr.ib(type=bool)
|
||||||
|
|
||||||
|
# the class of the user mapping provider
|
||||||
|
user_mapping_provider_class = attr.ib(type=Type)
|
||||||
|
|
||||||
|
# the config of the user mapping provider
|
||||||
|
user_mapping_provider_config = attr.ib()
|
||||||
|
@ -14,14 +14,13 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from distutils.util import strtobool
|
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
from synapse.api.constants import RoomCreationPreset
|
from synapse.api.constants import RoomCreationPreset
|
||||||
from synapse.config._base import Config, ConfigError
|
from synapse.config._base import Config, ConfigError
|
||||||
from synapse.types import RoomAlias, UserID
|
from synapse.types import RoomAlias, UserID
|
||||||
from synapse.util.stringutils import random_string_with_symbols
|
from synapse.util.stringutils import random_string_with_symbols, strtobool
|
||||||
|
|
||||||
|
|
||||||
class AccountValidityConfig(Config):
|
class AccountValidityConfig(Config):
|
||||||
@ -50,10 +49,6 @@ class AccountValidityConfig(Config):
|
|||||||
|
|
||||||
self.startup_job_max_delta = self.period * 10.0 / 100.0
|
self.startup_job_max_delta = self.period * 10.0 / 100.0
|
||||||
|
|
||||||
if self.renew_by_email_enabled:
|
|
||||||
if "public_baseurl" not in synapse_config:
|
|
||||||
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
|
|
||||||
|
|
||||||
template_dir = config.get("template_dir")
|
template_dir = config.get("template_dir")
|
||||||
|
|
||||||
if not template_dir:
|
if not template_dir:
|
||||||
@ -86,12 +81,12 @@ class RegistrationConfig(Config):
|
|||||||
section = "registration"
|
section = "registration"
|
||||||
|
|
||||||
def read_config(self, config, **kwargs):
|
def read_config(self, config, **kwargs):
|
||||||
self.enable_registration = bool(
|
self.enable_registration = strtobool(
|
||||||
strtobool(str(config.get("enable_registration", False)))
|
str(config.get("enable_registration", False))
|
||||||
)
|
)
|
||||||
if "disable_registration" in config:
|
if "disable_registration" in config:
|
||||||
self.enable_registration = not bool(
|
self.enable_registration = not strtobool(
|
||||||
strtobool(str(config["disable_registration"]))
|
str(config["disable_registration"])
|
||||||
)
|
)
|
||||||
|
|
||||||
self.account_validity = AccountValidityConfig(
|
self.account_validity = AccountValidityConfig(
|
||||||
@ -110,13 +105,6 @@ class RegistrationConfig(Config):
|
|||||||
account_threepid_delegates = config.get("account_threepid_delegates") or {}
|
account_threepid_delegates = config.get("account_threepid_delegates") or {}
|
||||||
self.account_threepid_delegate_email = account_threepid_delegates.get("email")
|
self.account_threepid_delegate_email = account_threepid_delegates.get("email")
|
||||||
self.account_threepid_delegate_msisdn = account_threepid_delegates.get("msisdn")
|
self.account_threepid_delegate_msisdn = account_threepid_delegates.get("msisdn")
|
||||||
if self.account_threepid_delegate_msisdn and not self.public_baseurl:
|
|
||||||
raise ConfigError(
|
|
||||||
"The configuration option `public_baseurl` is required if "
|
|
||||||
"`account_threepid_delegate.msisdn` is set, such that "
|
|
||||||
"clients know where to submit validation tokens to. Please "
|
|
||||||
"configure `public_baseurl`."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.default_identity_server = config.get("default_identity_server")
|
self.default_identity_server = config.get("default_identity_server")
|
||||||
self.allow_guest_access = config.get("allow_guest_access", False)
|
self.allow_guest_access = config.get("allow_guest_access", False)
|
||||||
@ -241,8 +229,9 @@ class RegistrationConfig(Config):
|
|||||||
# send an email to the account's email address with a renewal link. By
|
# send an email to the account's email address with a renewal link. By
|
||||||
# default, no such emails are sent.
|
# default, no such emails are sent.
|
||||||
#
|
#
|
||||||
# If you enable this setting, you will also need to fill out the 'email' and
|
# If you enable this setting, you will also need to fill out the 'email'
|
||||||
# 'public_baseurl' configuration sections.
|
# configuration section. You should also check that 'public_baseurl' is set
|
||||||
|
# correctly.
|
||||||
#
|
#
|
||||||
#renew_at: 1w
|
#renew_at: 1w
|
||||||
|
|
||||||
@ -333,8 +322,7 @@ class RegistrationConfig(Config):
|
|||||||
# The identity server which we suggest that clients should use when users log
|
# The identity server which we suggest that clients should use when users log
|
||||||
# in on this server.
|
# in on this server.
|
||||||
#
|
#
|
||||||
# (By default, no suggestion is made, so it is left up to the client.
|
# (By default, no suggestion is made, so it is left up to the client.)
|
||||||
# This setting is ignored unless public_baseurl is also set.)
|
|
||||||
#
|
#
|
||||||
#default_identity_server: https://matrix.org
|
#default_identity_server: https://matrix.org
|
||||||
|
|
||||||
@ -359,8 +347,6 @@ class RegistrationConfig(Config):
|
|||||||
# by the Matrix Identity Service API specification:
|
# by the Matrix Identity Service API specification:
|
||||||
# https://matrix.org/docs/spec/identity_service/latest
|
# https://matrix.org/docs/spec/identity_service/latest
|
||||||
#
|
#
|
||||||
# If a delegate is specified, the config option public_baseurl must also be filled out.
|
|
||||||
#
|
|
||||||
account_threepid_delegates:
|
account_threepid_delegates:
|
||||||
#email: https://example.com # Delegate email sending to example.com
|
#email: https://example.com # Delegate email sending to example.com
|
||||||
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
|
||||||
|
@ -189,8 +189,6 @@ class SAML2Config(Config):
|
|||||||
import saml2
|
import saml2
|
||||||
|
|
||||||
public_baseurl = self.public_baseurl
|
public_baseurl = self.public_baseurl
|
||||||
if public_baseurl is None:
|
|
||||||
raise ConfigError("saml2_config requires a public_baseurl to be set")
|
|
||||||
|
|
||||||
if self.saml2_grandfathered_mxid_source_attribute:
|
if self.saml2_grandfathered_mxid_source_attribute:
|
||||||
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
|
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
|
||||||
|
@ -26,7 +26,7 @@ import yaml
|
|||||||
from netaddr import IPSet
|
from netaddr import IPSet
|
||||||
|
|
||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||||
from synapse.http.endpoint import parse_and_validate_server_name
|
from synapse.util.stringutils import parse_and_validate_server_name
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
@ -161,7 +161,11 @@ class ServerConfig(Config):
|
|||||||
self.print_pidfile = config.get("print_pidfile")
|
self.print_pidfile = config.get("print_pidfile")
|
||||||
self.user_agent_suffix = config.get("user_agent_suffix")
|
self.user_agent_suffix = config.get("user_agent_suffix")
|
||||||
self.use_frozen_dicts = config.get("use_frozen_dicts", False)
|
self.use_frozen_dicts = config.get("use_frozen_dicts", False)
|
||||||
self.public_baseurl = config.get("public_baseurl")
|
self.public_baseurl = config.get("public_baseurl") or "https://%s/" % (
|
||||||
|
self.server_name,
|
||||||
|
)
|
||||||
|
if self.public_baseurl[-1] != "/":
|
||||||
|
self.public_baseurl += "/"
|
||||||
|
|
||||||
# Whether to enable user presence.
|
# Whether to enable user presence.
|
||||||
self.use_presence = config.get("use_presence", True)
|
self.use_presence = config.get("use_presence", True)
|
||||||
@ -317,9 +321,6 @@ class ServerConfig(Config):
|
|||||||
# Always blacklist 0.0.0.0, ::
|
# Always blacklist 0.0.0.0, ::
|
||||||
self.federation_ip_range_blacklist.update(["0.0.0.0", "::"])
|
self.federation_ip_range_blacklist.update(["0.0.0.0", "::"])
|
||||||
|
|
||||||
if self.public_baseurl is not None:
|
|
||||||
if self.public_baseurl[-1] != "/":
|
|
||||||
self.public_baseurl += "/"
|
|
||||||
self.start_pushers = config.get("start_pushers", True)
|
self.start_pushers = config.get("start_pushers", True)
|
||||||
|
|
||||||
# (undocumented) option for torturing the worker-mode replication a bit,
|
# (undocumented) option for torturing the worker-mode replication a bit,
|
||||||
@ -740,11 +741,16 @@ class ServerConfig(Config):
|
|||||||
#
|
#
|
||||||
#web_client_location: https://riot.example.com/
|
#web_client_location: https://riot.example.com/
|
||||||
|
|
||||||
# The public-facing base URL that clients use to access this HS
|
# The public-facing base URL that clients use to access this Homeserver (not
|
||||||
# (not including _matrix/...). This is the same URL a user would
|
# including _matrix/...). This is the same URL a user might enter into the
|
||||||
# enter into the 'custom HS URL' field on their client. If you
|
# 'Custom Homeserver URL' field on their client. If you use Synapse with a
|
||||||
# use synapse with a reverse proxy, this should be the URL to reach
|
# reverse proxy, this should be the URL to reach Synapse via the proxy.
|
||||||
# synapse via the proxy.
|
# Otherwise, it should be the URL to reach Synapse's client HTTP listener (see
|
||||||
|
# 'listeners' below).
|
||||||
|
#
|
||||||
|
# If this is left unset, it defaults to 'https://<server_name>/'. (Note that
|
||||||
|
# that will not work unless you configure Synapse or a reverse-proxy to listen
|
||||||
|
# on port 443.)
|
||||||
#
|
#
|
||||||
#public_baseurl: https://example.com/
|
#public_baseurl: https://example.com/
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ class SSOConfig(Config):
|
|||||||
self.sso_error_template,
|
self.sso_error_template,
|
||||||
sso_account_deactivated_template,
|
sso_account_deactivated_template,
|
||||||
sso_auth_success_template,
|
sso_auth_success_template,
|
||||||
|
self.sso_auth_bad_user_template,
|
||||||
) = self.read_templates(
|
) = self.read_templates(
|
||||||
[
|
[
|
||||||
"sso_login_idp_picker.html",
|
"sso_login_idp_picker.html",
|
||||||
@ -45,6 +46,7 @@ class SSOConfig(Config):
|
|||||||
"sso_error.html",
|
"sso_error.html",
|
||||||
"sso_account_deactivated.html",
|
"sso_account_deactivated.html",
|
||||||
"sso_auth_success.html",
|
"sso_auth_success.html",
|
||||||
|
"sso_auth_bad_user.html",
|
||||||
],
|
],
|
||||||
template_dir,
|
template_dir,
|
||||||
)
|
)
|
||||||
@ -62,11 +64,8 @@ class SSOConfig(Config):
|
|||||||
# gracefully to the client). This would make it pointless to ask the user for
|
# gracefully to the client). This would make it pointless to ask the user for
|
||||||
# confirmation, since the URL the confirmation page would be showing wouldn't be
|
# confirmation, since the URL the confirmation page would be showing wouldn't be
|
||||||
# the client's.
|
# the client's.
|
||||||
# public_baseurl is an optional setting, so we only add the fallback's URL to the
|
login_fallback_url = self.public_baseurl + "_matrix/static/client/login"
|
||||||
# list if it's provided (because we can't figure out what that URL is otherwise).
|
self.sso_client_whitelist.append(login_fallback_url)
|
||||||
if self.public_baseurl:
|
|
||||||
login_fallback_url = self.public_baseurl + "_matrix/static/client/login"
|
|
||||||
self.sso_client_whitelist.append(login_fallback_url)
|
|
||||||
|
|
||||||
def generate_config_section(self, **kwargs):
|
def generate_config_section(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
@ -84,9 +83,9 @@ class SSOConfig(Config):
|
|||||||
# phishing attacks from evil.site. To avoid this, include a slash after the
|
# phishing attacks from evil.site. To avoid this, include a slash after the
|
||||||
# hostname: "https://my.client/".
|
# hostname: "https://my.client/".
|
||||||
#
|
#
|
||||||
# If public_baseurl is set, then the login fallback page (used by clients
|
# The login fallback page (used by clients that don't natively support the
|
||||||
# that don't natively support the required login flows) is whitelisted in
|
# required login flows) is automatically whitelisted in addition to any URLs
|
||||||
# addition to any URLs in this list.
|
# in this list.
|
||||||
#
|
#
|
||||||
# By default, this list is empty.
|
# By default, this list is empty.
|
||||||
#
|
#
|
||||||
@ -160,6 +159,14 @@ class SSOConfig(Config):
|
|||||||
#
|
#
|
||||||
# This template has no additional variables.
|
# This template has no additional variables.
|
||||||
#
|
#
|
||||||
|
# * HTML page shown after a user-interactive authentication session which
|
||||||
|
# does not map correctly onto the expected user: 'sso_auth_bad_user.html'.
|
||||||
|
#
|
||||||
|
# When rendering, this template is given the following variables:
|
||||||
|
# * server_name: the homeserver's name.
|
||||||
|
# * user_id_to_verify: the MXID of the user that we are trying to
|
||||||
|
# validate.
|
||||||
|
#
|
||||||
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
|
# * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
|
||||||
# attempts to login: 'sso_account_deactivated.html'.
|
# attempts to login: 'sso_account_deactivated.html'.
|
||||||
#
|
#
|
||||||
|
@ -56,6 +56,12 @@ class WriterLocations:
|
|||||||
to_device = attr.ib(
|
to_device = attr.ib(
|
||||||
default=["master"], type=List[str], converter=_instance_to_list_converter,
|
default=["master"], type=List[str], converter=_instance_to_list_converter,
|
||||||
)
|
)
|
||||||
|
account_data = attr.ib(
|
||||||
|
default=["master"], type=List[str], converter=_instance_to_list_converter,
|
||||||
|
)
|
||||||
|
receipts = attr.ib(
|
||||||
|
default=["master"], type=List[str], converter=_instance_to_list_converter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkerConfig(Config):
|
class WorkerConfig(Config):
|
||||||
@ -127,7 +133,7 @@ class WorkerConfig(Config):
|
|||||||
|
|
||||||
# Check that the configured writers for events and typing also appears in
|
# Check that the configured writers for events and typing also appears in
|
||||||
# `instance_map`.
|
# `instance_map`.
|
||||||
for stream in ("events", "typing", "to_device"):
|
for stream in ("events", "typing", "to_device", "account_data", "receipts"):
|
||||||
instances = _instance_to_list_converter(getattr(self.writers, stream))
|
instances = _instance_to_list_converter(getattr(self.writers, stream))
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
if instance != "master" and instance not in self.instance_map:
|
if instance != "master" and instance not in self.instance_map:
|
||||||
@ -141,6 +147,16 @@ class WorkerConfig(Config):
|
|||||||
"Must only specify one instance to handle `to_device` messages."
|
"Must only specify one instance to handle `to_device` messages."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if len(self.writers.account_data) != 1:
|
||||||
|
raise ConfigError(
|
||||||
|
"Must only specify one instance to handle `account_data` messages."
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(self.writers.receipts) != 1:
|
||||||
|
raise ConfigError(
|
||||||
|
"Must only specify one instance to handle `receipts` messages."
|
||||||
|
)
|
||||||
|
|
||||||
self.events_shard_config = ShardedWorkerHandlingConfig(self.writers.events)
|
self.events_shard_config = ShardedWorkerHandlingConfig(self.writers.events)
|
||||||
|
|
||||||
# Whether this worker should run background tasks or not.
|
# Whether this worker should run background tasks or not.
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
import os
|
import os
|
||||||
from distutils.util import strtobool
|
|
||||||
from typing import Dict, Optional, Tuple, Type
|
from typing import Dict, Optional, Tuple, Type
|
||||||
|
|
||||||
from unpaddedbase64 import encode_base64
|
from unpaddedbase64 import encode_base64
|
||||||
@ -26,6 +25,7 @@ from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVers
|
|||||||
from synapse.types import JsonDict, RoomStreamToken
|
from synapse.types import JsonDict, RoomStreamToken
|
||||||
from synapse.util.caches import intern_dict
|
from synapse.util.caches import intern_dict
|
||||||
from synapse.util.frozenutils import freeze
|
from synapse.util.frozenutils import freeze
|
||||||
|
from synapse.util.stringutils import strtobool
|
||||||
|
|
||||||
# Whether we should use frozen_dict in FrozenEvent. Using frozen_dicts prevents
|
# Whether we should use frozen_dict in FrozenEvent. Using frozen_dicts prevents
|
||||||
# bugs where we accidentally share e.g. signature dicts. However, converting a
|
# bugs where we accidentally share e.g. signature dicts. However, converting a
|
||||||
@ -34,6 +34,7 @@ from synapse.util.frozenutils import freeze
|
|||||||
# NOTE: This is overridden by the configuration by the Synapse worker apps, but
|
# NOTE: This is overridden by the configuration by the Synapse worker apps, but
|
||||||
# for the sake of tests, it is set here while it cannot be configured on the
|
# for the sake of tests, it is set here while it cannot be configured on the
|
||||||
# homeserver object itself.
|
# homeserver object itself.
|
||||||
|
|
||||||
USE_FROZEN_DICTS = strtobool(os.environ.get("SYNAPSE_USE_FROZEN_DICTS", "0"))
|
USE_FROZEN_DICTS = strtobool(os.environ.get("SYNAPSE_USE_FROZEN_DICTS", "0"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import copy
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
@ -26,7 +27,6 @@ from typing import (
|
|||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
|
||||||
Tuple,
|
Tuple,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
@ -61,6 +61,9 @@ from synapse.util import unwrapFirstError
|
|||||||
from synapse.util.caches.expiringcache import ExpiringCache
|
from synapse.util.caches.expiringcache import ExpiringCache
|
||||||
from synapse.util.retryutils import NotRetryingDestination
|
from synapse.util.retryutils import NotRetryingDestination
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from synapse.app.homeserver import HomeServer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
sent_queries_counter = Counter("synapse_federation_client_sent_queries", "", ["type"])
|
sent_queries_counter = Counter("synapse_federation_client_sent_queries", "", ["type"])
|
||||||
@ -80,10 +83,10 @@ class InvalidResponseError(RuntimeError):
|
|||||||
|
|
||||||
|
|
||||||
class FederationClient(FederationBase):
|
class FederationClient(FederationBase):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__(hs)
|
super().__init__(hs)
|
||||||
|
|
||||||
self.pdu_destination_tried = {}
|
self.pdu_destination_tried = {} # type: Dict[str, Dict[str, int]]
|
||||||
self._clock.looping_call(self._clear_tried_cache, 60 * 1000)
|
self._clock.looping_call(self._clear_tried_cache, 60 * 1000)
|
||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
self.transport_layer = hs.get_federation_transport_client()
|
self.transport_layer = hs.get_federation_transport_client()
|
||||||
@ -116,33 +119,32 @@ class FederationClient(FederationBase):
|
|||||||
self.pdu_destination_tried[event_id] = destination_dict
|
self.pdu_destination_tried[event_id] = destination_dict
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def make_query(
|
async def make_query(
|
||||||
self,
|
self,
|
||||||
destination,
|
destination: str,
|
||||||
query_type,
|
query_type: str,
|
||||||
args,
|
args: dict,
|
||||||
retry_on_dns_fail=False,
|
retry_on_dns_fail: bool = False,
|
||||||
ignore_backoff=False,
|
ignore_backoff: bool = False,
|
||||||
):
|
) -> JsonDict:
|
||||||
"""Sends a federation Query to a remote homeserver of the given type
|
"""Sends a federation Query to a remote homeserver of the given type
|
||||||
and arguments.
|
and arguments.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
destination (str): Domain name of the remote homeserver
|
destination: Domain name of the remote homeserver
|
||||||
query_type (str): Category of the query type; should match the
|
query_type: Category of the query type; should match the
|
||||||
handler name used in register_query_handler().
|
handler name used in register_query_handler().
|
||||||
args (dict): Mapping of strings to strings containing the details
|
args: Mapping of strings to strings containing the details
|
||||||
of the query request.
|
of the query request.
|
||||||
ignore_backoff (bool): true to ignore the historical backoff data
|
ignore_backoff: true to ignore the historical backoff data
|
||||||
and try the request anyway.
|
and try the request anyway.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
a Awaitable which will eventually yield a JSON object from the
|
The JSON object from the response
|
||||||
response
|
|
||||||
"""
|
"""
|
||||||
sent_queries_counter.labels(query_type).inc()
|
sent_queries_counter.labels(query_type).inc()
|
||||||
|
|
||||||
return self.transport_layer.make_query(
|
return await self.transport_layer.make_query(
|
||||||
destination,
|
destination,
|
||||||
query_type,
|
query_type,
|
||||||
args,
|
args,
|
||||||
@ -151,42 +153,52 @@ class FederationClient(FederationBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def query_client_keys(self, destination, content, timeout):
|
async def query_client_keys(
|
||||||
|
self, destination: str, content: JsonDict, timeout: int
|
||||||
|
) -> JsonDict:
|
||||||
"""Query device keys for a device hosted on a remote server.
|
"""Query device keys for a device hosted on a remote server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
destination (str): Domain name of the remote homeserver
|
destination: Domain name of the remote homeserver
|
||||||
content (dict): The query content.
|
content: The query content.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
an Awaitable which will eventually yield a JSON object from the
|
The JSON object from the response
|
||||||
response
|
|
||||||
"""
|
"""
|
||||||
sent_queries_counter.labels("client_device_keys").inc()
|
sent_queries_counter.labels("client_device_keys").inc()
|
||||||
return self.transport_layer.query_client_keys(destination, content, timeout)
|
return await self.transport_layer.query_client_keys(
|
||||||
|
destination, content, timeout
|
||||||
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def query_user_devices(self, destination, user_id, timeout=30000):
|
async def query_user_devices(
|
||||||
|
self, destination: str, user_id: str, timeout: int = 30000
|
||||||
|
) -> JsonDict:
|
||||||
"""Query the device keys for a list of user ids hosted on a remote
|
"""Query the device keys for a list of user ids hosted on a remote
|
||||||
server.
|
server.
|
||||||
"""
|
"""
|
||||||
sent_queries_counter.labels("user_devices").inc()
|
sent_queries_counter.labels("user_devices").inc()
|
||||||
return self.transport_layer.query_user_devices(destination, user_id, timeout)
|
return await self.transport_layer.query_user_devices(
|
||||||
|
destination, user_id, timeout
|
||||||
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def claim_client_keys(self, destination, content, timeout):
|
async def claim_client_keys(
|
||||||
|
self, destination: str, content: JsonDict, timeout: int
|
||||||
|
) -> JsonDict:
|
||||||
"""Claims one-time keys for a device hosted on a remote server.
|
"""Claims one-time keys for a device hosted on a remote server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
destination (str): Domain name of the remote homeserver
|
destination: Domain name of the remote homeserver
|
||||||
content (dict): The query content.
|
content: The query content.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
an Awaitable which will eventually yield a JSON object from the
|
The JSON object from the response
|
||||||
response
|
|
||||||
"""
|
"""
|
||||||
sent_queries_counter.labels("client_one_time_keys").inc()
|
sent_queries_counter.labels("client_one_time_keys").inc()
|
||||||
return self.transport_layer.claim_client_keys(destination, content, timeout)
|
return await self.transport_layer.claim_client_keys(
|
||||||
|
destination, content, timeout
|
||||||
|
)
|
||||||
|
|
||||||
async def backfill(
|
async def backfill(
|
||||||
self, dest: str, room_id: str, limit: int, extremities: Iterable[str]
|
self, dest: str, room_id: str, limit: int, extremities: Iterable[str]
|
||||||
@ -195,10 +207,10 @@ class FederationClient(FederationBase):
|
|||||||
given destination server.
|
given destination server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dest (str): The remote homeserver to ask.
|
dest: The remote homeserver to ask.
|
||||||
room_id (str): The room_id to backfill.
|
room_id: The room_id to backfill.
|
||||||
limit (int): The maximum number of events to return.
|
limit: The maximum number of events to return.
|
||||||
extremities (list): our current backwards extremities, to backfill from
|
extremities: our current backwards extremities, to backfill from
|
||||||
"""
|
"""
|
||||||
logger.debug("backfill extrem=%s", extremities)
|
logger.debug("backfill extrem=%s", extremities)
|
||||||
|
|
||||||
@ -370,7 +382,7 @@ class FederationClient(FederationBase):
|
|||||||
for events that have failed their checks
|
for events that have failed their checks
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred : A list of PDUs that have valid signatures and hashes.
|
A list of PDUs that have valid signatures and hashes.
|
||||||
"""
|
"""
|
||||||
deferreds = self._check_sigs_and_hashes(room_version, pdus)
|
deferreds = self._check_sigs_and_hashes(room_version, pdus)
|
||||||
|
|
||||||
@ -418,7 +430,9 @@ class FederationClient(FederationBase):
|
|||||||
else:
|
else:
|
||||||
return [p for p in valid_pdus if p]
|
return [p for p in valid_pdus if p]
|
||||||
|
|
||||||
async def get_event_auth(self, destination, room_id, event_id):
|
async def get_event_auth(
|
||||||
|
self, destination: str, room_id: str, event_id: str
|
||||||
|
) -> List[EventBase]:
|
||||||
res = await self.transport_layer.get_event_auth(destination, room_id, event_id)
|
res = await self.transport_layer.get_event_auth(destination, room_id, event_id)
|
||||||
|
|
||||||
room_version = await self.store.get_room_version(room_id)
|
room_version = await self.store.get_room_version(room_id)
|
||||||
@ -700,18 +714,16 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
return await self._try_destination_list("send_join", destinations, send_request)
|
return await self._try_destination_list("send_join", destinations, send_request)
|
||||||
|
|
||||||
async def _do_send_join(self, destination: str, pdu: EventBase):
|
async def _do_send_join(self, destination: str, pdu: EventBase) -> JsonDict:
|
||||||
time_now = self._clock.time_msec()
|
time_now = self._clock.time_msec()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = await self.transport_layer.send_join_v2(
|
return await self.transport_layer.send_join_v2(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
room_id=pdu.room_id,
|
room_id=pdu.room_id,
|
||||||
event_id=pdu.event_id,
|
event_id=pdu.event_id,
|
||||||
content=pdu.get_pdu_json(time_now),
|
content=pdu.get_pdu_json(time_now),
|
||||||
)
|
)
|
||||||
|
|
||||||
return content
|
|
||||||
except HttpResponseException as e:
|
except HttpResponseException as e:
|
||||||
if e.code in [400, 404]:
|
if e.code in [400, 404]:
|
||||||
err = e.to_synapse_error()
|
err = e.to_synapse_error()
|
||||||
@ -769,7 +781,7 @@ class FederationClient(FederationBase):
|
|||||||
time_now = self._clock.time_msec()
|
time_now = self._clock.time_msec()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = await self.transport_layer.send_invite_v2(
|
return await self.transport_layer.send_invite_v2(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
room_id=pdu.room_id,
|
room_id=pdu.room_id,
|
||||||
event_id=pdu.event_id,
|
event_id=pdu.event_id,
|
||||||
@ -779,7 +791,6 @@ class FederationClient(FederationBase):
|
|||||||
"invite_room_state": pdu.unsigned.get("invite_room_state", []),
|
"invite_room_state": pdu.unsigned.get("invite_room_state", []),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return content
|
|
||||||
except HttpResponseException as e:
|
except HttpResponseException as e:
|
||||||
if e.code in [400, 404]:
|
if e.code in [400, 404]:
|
||||||
err = e.to_synapse_error()
|
err = e.to_synapse_error()
|
||||||
@ -842,18 +853,16 @@ class FederationClient(FederationBase):
|
|||||||
"send_leave", destinations, send_request
|
"send_leave", destinations, send_request
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _do_send_leave(self, destination, pdu):
|
async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict:
|
||||||
time_now = self._clock.time_msec()
|
time_now = self._clock.time_msec()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content = await self.transport_layer.send_leave_v2(
|
return await self.transport_layer.send_leave_v2(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
room_id=pdu.room_id,
|
room_id=pdu.room_id,
|
||||||
event_id=pdu.event_id,
|
event_id=pdu.event_id,
|
||||||
content=pdu.get_pdu_json(time_now),
|
content=pdu.get_pdu_json(time_now),
|
||||||
)
|
)
|
||||||
|
|
||||||
return content
|
|
||||||
except HttpResponseException as e:
|
except HttpResponseException as e:
|
||||||
if e.code in [400, 404]:
|
if e.code in [400, 404]:
|
||||||
err = e.to_synapse_error()
|
err = e.to_synapse_error()
|
||||||
@ -879,7 +888,7 @@ class FederationClient(FederationBase):
|
|||||||
# content.
|
# content.
|
||||||
return resp[1]
|
return resp[1]
|
||||||
|
|
||||||
def get_public_rooms(
|
async def get_public_rooms(
|
||||||
self,
|
self,
|
||||||
remote_server: str,
|
remote_server: str,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
@ -887,7 +896,7 @@ class FederationClient(FederationBase):
|
|||||||
search_filter: Optional[Dict] = None,
|
search_filter: Optional[Dict] = None,
|
||||||
include_all_networks: bool = False,
|
include_all_networks: bool = False,
|
||||||
third_party_instance_id: Optional[str] = None,
|
third_party_instance_id: Optional[str] = None,
|
||||||
):
|
) -> JsonDict:
|
||||||
"""Get the list of public rooms from a remote homeserver
|
"""Get the list of public rooms from a remote homeserver
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -901,8 +910,7 @@ class FederationClient(FederationBase):
|
|||||||
party instance
|
party instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Awaitable[Dict[str, Any]]: The response from the remote server, or None if
|
The response from the remote server.
|
||||||
`remote_server` is the same as the local server_name
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HttpResponseException: There was an exception returned from the remote server
|
HttpResponseException: There was an exception returned from the remote server
|
||||||
@ -910,7 +918,7 @@ class FederationClient(FederationBase):
|
|||||||
requests over federation
|
requests over federation
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.transport_layer.get_public_rooms(
|
return await self.transport_layer.get_public_rooms(
|
||||||
remote_server,
|
remote_server,
|
||||||
limit,
|
limit,
|
||||||
since_token,
|
since_token,
|
||||||
@ -923,7 +931,7 @@ class FederationClient(FederationBase):
|
|||||||
self,
|
self,
|
||||||
destination: str,
|
destination: str,
|
||||||
room_id: str,
|
room_id: str,
|
||||||
earliest_events_ids: Sequence[str],
|
earliest_events_ids: Iterable[str],
|
||||||
latest_events: Iterable[EventBase],
|
latest_events: Iterable[EventBase],
|
||||||
limit: int,
|
limit: int,
|
||||||
min_depth: int,
|
min_depth: int,
|
||||||
@ -974,7 +982,9 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
return signed_events
|
return signed_events
|
||||||
|
|
||||||
async def forward_third_party_invite(self, destinations, room_id, event_dict):
|
async def forward_third_party_invite(
|
||||||
|
self, destinations: Iterable[str], room_id: str, event_dict: JsonDict
|
||||||
|
) -> None:
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
if destination == self.server_name:
|
if destination == self.server_name:
|
||||||
continue
|
continue
|
||||||
@ -983,7 +993,7 @@ class FederationClient(FederationBase):
|
|||||||
await self.transport_layer.exchange_third_party_invite(
|
await self.transport_layer.exchange_third_party_invite(
|
||||||
destination=destination, room_id=room_id, event_dict=event_dict
|
destination=destination, room_id=room_id, event_dict=event_dict
|
||||||
)
|
)
|
||||||
return None
|
return
|
||||||
except CodeMessageException:
|
except CodeMessageException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -995,7 +1005,7 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
async def get_room_complexity(
|
async def get_room_complexity(
|
||||||
self, destination: str, room_id: str
|
self, destination: str, room_id: str
|
||||||
) -> Optional[dict]:
|
) -> Optional[JsonDict]:
|
||||||
"""
|
"""
|
||||||
Fetch the complexity of a remote room from another server.
|
Fetch the complexity of a remote room from another server.
|
||||||
|
|
||||||
@ -1008,10 +1018,9 @@ class FederationClient(FederationBase):
|
|||||||
could not fetch the complexity.
|
could not fetch the complexity.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
complexity = await self.transport_layer.get_room_complexity(
|
return await self.transport_layer.get_room_complexity(
|
||||||
destination=destination, room_id=room_id
|
destination=destination, room_id=room_id
|
||||||
)
|
)
|
||||||
return complexity
|
|
||||||
except CodeMessageException as e:
|
except CodeMessageException as e:
|
||||||
# We didn't manage to get it -- probably a 404. We are okay if other
|
# We didn't manage to get it -- probably a 404. We are okay if other
|
||||||
# servers don't give it to us.
|
# servers don't give it to us.
|
||||||
|
@ -49,7 +49,6 @@ from synapse.events import EventBase
|
|||||||
from synapse.federation.federation_base import FederationBase, event_from_pdu_json
|
from synapse.federation.federation_base import FederationBase, event_from_pdu_json
|
||||||
from synapse.federation.persistence import TransactionActions
|
from synapse.federation.persistence import TransactionActions
|
||||||
from synapse.federation.units import Edu, Transaction
|
from synapse.federation.units import Edu, Transaction
|
||||||
from synapse.http.endpoint import parse_server_name
|
|
||||||
from synapse.http.servlet import assert_params_in_dict
|
from synapse.http.servlet import assert_params_in_dict
|
||||||
from synapse.logging.context import (
|
from synapse.logging.context import (
|
||||||
make_deferred_yieldable,
|
make_deferred_yieldable,
|
||||||
@ -66,6 +65,7 @@ from synapse.types import JsonDict, get_domain_from_id
|
|||||||
from synapse.util import glob_to_regex, json_decoder, unwrapFirstError
|
from synapse.util import glob_to_regex, json_decoder, unwrapFirstError
|
||||||
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
||||||
from synapse.util.caches.response_cache import ResponseCache
|
from synapse.util.caches.response_cache import ResponseCache
|
||||||
|
from synapse.util.stringutils import parse_server_name
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -28,7 +28,6 @@ from synapse.api.urls import (
|
|||||||
FEDERATION_V1_PREFIX,
|
FEDERATION_V1_PREFIX,
|
||||||
FEDERATION_V2_PREFIX,
|
FEDERATION_V2_PREFIX,
|
||||||
)
|
)
|
||||||
from synapse.http.endpoint import parse_and_validate_server_name
|
|
||||||
from synapse.http.server import JsonResource
|
from synapse.http.server import JsonResource
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
parse_boolean_from_args,
|
parse_boolean_from_args,
|
||||||
@ -45,6 +44,7 @@ from synapse.logging.opentracing import (
|
|||||||
)
|
)
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
from synapse.types import ThirdPartyInstanceID, get_domain_from_id
|
from synapse.types import ThirdPartyInstanceID, get_domain_from_id
|
||||||
|
from synapse.util.stringutils import parse_and_validate_server_name
|
||||||
from synapse.util.versionstring import get_version_string
|
from synapse.util.versionstring import get_version_string
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
# Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -12,14 +13,157 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import random
|
||||||
from typing import TYPE_CHECKING, List, Tuple
|
from typing import TYPE_CHECKING, List, Tuple
|
||||||
|
|
||||||
|
from synapse.replication.http.account_data import (
|
||||||
|
ReplicationAddTagRestServlet,
|
||||||
|
ReplicationRemoveTagRestServlet,
|
||||||
|
ReplicationRoomAccountDataRestServlet,
|
||||||
|
ReplicationUserAccountDataRestServlet,
|
||||||
|
)
|
||||||
from synapse.types import JsonDict, UserID
|
from synapse.types import JsonDict, UserID
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.app.homeserver import HomeServer
|
from synapse.app.homeserver import HomeServer
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDataHandler:
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._store = hs.get_datastore()
|
||||||
|
self._instance_name = hs.get_instance_name()
|
||||||
|
self._notifier = hs.get_notifier()
|
||||||
|
|
||||||
|
self._user_data_client = ReplicationUserAccountDataRestServlet.make_client(hs)
|
||||||
|
self._room_data_client = ReplicationRoomAccountDataRestServlet.make_client(hs)
|
||||||
|
self._add_tag_client = ReplicationAddTagRestServlet.make_client(hs)
|
||||||
|
self._remove_tag_client = ReplicationRemoveTagRestServlet.make_client(hs)
|
||||||
|
self._account_data_writers = hs.config.worker.writers.account_data
|
||||||
|
|
||||||
|
async def add_account_data_to_room(
|
||||||
|
self, user_id: str, room_id: str, account_data_type: str, content: JsonDict
|
||||||
|
) -> int:
|
||||||
|
"""Add some account_data to a room for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user to add a tag for.
|
||||||
|
room_id: The room to add a tag for.
|
||||||
|
account_data_type: The type of account_data to add.
|
||||||
|
content: A json object to associate with the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The maximum stream ID.
|
||||||
|
"""
|
||||||
|
if self._instance_name in self._account_data_writers:
|
||||||
|
max_stream_id = await self._store.add_account_data_to_room(
|
||||||
|
user_id, room_id, account_data_type, content
|
||||||
|
)
|
||||||
|
|
||||||
|
self._notifier.on_new_event(
|
||||||
|
"account_data_key", max_stream_id, users=[user_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return max_stream_id
|
||||||
|
else:
|
||||||
|
response = await self._room_data_client(
|
||||||
|
instance_name=random.choice(self._account_data_writers),
|
||||||
|
user_id=user_id,
|
||||||
|
room_id=room_id,
|
||||||
|
account_data_type=account_data_type,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
return response["max_stream_id"]
|
||||||
|
|
||||||
|
async def add_account_data_for_user(
|
||||||
|
self, user_id: str, account_data_type: str, content: JsonDict
|
||||||
|
) -> int:
|
||||||
|
"""Add some account_data to a room for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user to add a tag for.
|
||||||
|
account_data_type: The type of account_data to add.
|
||||||
|
content: A json object to associate with the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The maximum stream ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._instance_name in self._account_data_writers:
|
||||||
|
max_stream_id = await self._store.add_account_data_for_user(
|
||||||
|
user_id, account_data_type, content
|
||||||
|
)
|
||||||
|
|
||||||
|
self._notifier.on_new_event(
|
||||||
|
"account_data_key", max_stream_id, users=[user_id]
|
||||||
|
)
|
||||||
|
return max_stream_id
|
||||||
|
else:
|
||||||
|
response = await self._user_data_client(
|
||||||
|
instance_name=random.choice(self._account_data_writers),
|
||||||
|
user_id=user_id,
|
||||||
|
account_data_type=account_data_type,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
return response["max_stream_id"]
|
||||||
|
|
||||||
|
async def add_tag_to_room(
|
||||||
|
self, user_id: str, room_id: str, tag: str, content: JsonDict
|
||||||
|
) -> int:
|
||||||
|
"""Add a tag to a room for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user to add a tag for.
|
||||||
|
room_id: The room to add a tag for.
|
||||||
|
tag: The tag name to add.
|
||||||
|
content: A json object to associate with the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The next account data ID.
|
||||||
|
"""
|
||||||
|
if self._instance_name in self._account_data_writers:
|
||||||
|
max_stream_id = await self._store.add_tag_to_room(
|
||||||
|
user_id, room_id, tag, content
|
||||||
|
)
|
||||||
|
|
||||||
|
self._notifier.on_new_event(
|
||||||
|
"account_data_key", max_stream_id, users=[user_id]
|
||||||
|
)
|
||||||
|
return max_stream_id
|
||||||
|
else:
|
||||||
|
response = await self._add_tag_client(
|
||||||
|
instance_name=random.choice(self._account_data_writers),
|
||||||
|
user_id=user_id,
|
||||||
|
room_id=room_id,
|
||||||
|
tag=tag,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
return response["max_stream_id"]
|
||||||
|
|
||||||
|
async def remove_tag_from_room(self, user_id: str, room_id: str, tag: str) -> int:
|
||||||
|
"""Remove a tag from a room for a user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The next account data ID.
|
||||||
|
"""
|
||||||
|
if self._instance_name in self._account_data_writers:
|
||||||
|
max_stream_id = await self._store.remove_tag_from_room(
|
||||||
|
user_id, room_id, tag
|
||||||
|
)
|
||||||
|
|
||||||
|
self._notifier.on_new_event(
|
||||||
|
"account_data_key", max_stream_id, users=[user_id]
|
||||||
|
)
|
||||||
|
return max_stream_id
|
||||||
|
else:
|
||||||
|
response = await self._remove_tag_client(
|
||||||
|
instance_name=random.choice(self._account_data_writers),
|
||||||
|
user_id=user_id,
|
||||||
|
room_id=room_id,
|
||||||
|
tag=tag,
|
||||||
|
)
|
||||||
|
return response["max_stream_id"]
|
||||||
|
|
||||||
|
|
||||||
class AccountDataEventSource:
|
class AccountDataEventSource:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
@ -49,8 +49,13 @@ from synapse.api.errors import (
|
|||||||
UserDeactivatedError,
|
UserDeactivatedError,
|
||||||
)
|
)
|
||||||
from synapse.api.ratelimiting import Ratelimiter
|
from synapse.api.ratelimiting import Ratelimiter
|
||||||
from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
|
from synapse.handlers._base import BaseHandler
|
||||||
|
from synapse.handlers.ui_auth import (
|
||||||
|
INTERACTIVE_AUTH_CHECKERS,
|
||||||
|
UIAuthSessionDataConstants,
|
||||||
|
)
|
||||||
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
|
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
|
||||||
|
from synapse.http import get_request_user_agent
|
||||||
from synapse.http.server import finish_request, respond_with_html
|
from synapse.http.server import finish_request, respond_with_html
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging.context import defer_to_thread
|
from synapse.logging.context import defer_to_thread
|
||||||
@ -62,8 +67,6 @@ from synapse.util.async_helpers import maybe_awaitable
|
|||||||
from synapse.util.msisdn import phone_number_to_msisdn
|
from synapse.util.msisdn import phone_number_to_msisdn
|
||||||
from synapse.util.threepids import canonicalise_email
|
from synapse.util.threepids import canonicalise_email
|
||||||
|
|
||||||
from ._base import BaseHandler
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.app.homeserver import HomeServer
|
from synapse.app.homeserver import HomeServer
|
||||||
|
|
||||||
@ -260,10 +263,6 @@ class AuthHandler(BaseHandler):
|
|||||||
# authenticating for an operation to occur on their account.
|
# authenticating for an operation to occur on their account.
|
||||||
self._sso_auth_confirm_template = hs.config.sso_auth_confirm_template
|
self._sso_auth_confirm_template = hs.config.sso_auth_confirm_template
|
||||||
|
|
||||||
# The following template is shown after a successful user interactive
|
|
||||||
# authentication session. It tells the user they can close the window.
|
|
||||||
self._sso_auth_success_template = hs.config.sso_auth_success_template
|
|
||||||
|
|
||||||
# The following template is shown during the SSO authentication process if
|
# The following template is shown during the SSO authentication process if
|
||||||
# the account is deactivated.
|
# the account is deactivated.
|
||||||
self._sso_account_deactivated_template = (
|
self._sso_account_deactivated_template = (
|
||||||
@ -284,7 +283,6 @@ class AuthHandler(BaseHandler):
|
|||||||
requester: Requester,
|
requester: Requester,
|
||||||
request: SynapseRequest,
|
request: SynapseRequest,
|
||||||
request_body: Dict[str, Any],
|
request_body: Dict[str, Any],
|
||||||
clientip: str,
|
|
||||||
description: str,
|
description: str,
|
||||||
) -> Tuple[dict, Optional[str]]:
|
) -> Tuple[dict, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
@ -301,8 +299,6 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
request_body: The body of the request sent by the client
|
request_body: The body of the request sent by the client
|
||||||
|
|
||||||
clientip: The IP address of the client.
|
|
||||||
|
|
||||||
description: A human readable string to be displayed to the user that
|
description: A human readable string to be displayed to the user that
|
||||||
describes the operation happening on their account.
|
describes the operation happening on their account.
|
||||||
|
|
||||||
@ -338,10 +334,10 @@ class AuthHandler(BaseHandler):
|
|||||||
request_body.pop("auth", None)
|
request_body.pop("auth", None)
|
||||||
return request_body, None
|
return request_body, None
|
||||||
|
|
||||||
user_id = requester.user.to_string()
|
requester_user_id = requester.user.to_string()
|
||||||
|
|
||||||
# Check if we should be ratelimited due to too many previous failed attempts
|
# Check if we should be ratelimited due to too many previous failed attempts
|
||||||
self._failed_uia_attempts_ratelimiter.ratelimit(user_id, update=False)
|
self._failed_uia_attempts_ratelimiter.ratelimit(requester_user_id, update=False)
|
||||||
|
|
||||||
# build a list of supported flows
|
# build a list of supported flows
|
||||||
supported_ui_auth_types = await self._get_available_ui_auth_types(
|
supported_ui_auth_types = await self._get_available_ui_auth_types(
|
||||||
@ -349,13 +345,16 @@ class AuthHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
flows = [[login_type] for login_type in supported_ui_auth_types]
|
flows = [[login_type] for login_type in supported_ui_auth_types]
|
||||||
|
|
||||||
|
def get_new_session_data() -> JsonDict:
|
||||||
|
return {UIAuthSessionDataConstants.REQUEST_USER_ID: requester_user_id}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result, params, session_id = await self.check_ui_auth(
|
result, params, session_id = await self.check_ui_auth(
|
||||||
flows, request, request_body, clientip, description
|
flows, request, request_body, description, get_new_session_data,
|
||||||
)
|
)
|
||||||
except LoginError:
|
except LoginError:
|
||||||
# Update the ratelimiter to say we failed (`can_do_action` doesn't raise).
|
# Update the ratelimiter to say we failed (`can_do_action` doesn't raise).
|
||||||
self._failed_uia_attempts_ratelimiter.can_do_action(user_id)
|
self._failed_uia_attempts_ratelimiter.can_do_action(requester_user_id)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# find the completed login type
|
# find the completed login type
|
||||||
@ -363,14 +362,14 @@ class AuthHandler(BaseHandler):
|
|||||||
if login_type not in result:
|
if login_type not in result:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
user_id = result[login_type]
|
validated_user_id = result[login_type]
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# this can't happen
|
# this can't happen
|
||||||
raise Exception("check_auth returned True but no successful login type")
|
raise Exception("check_auth returned True but no successful login type")
|
||||||
|
|
||||||
# check that the UI auth matched the access token
|
# check that the UI auth matched the access token
|
||||||
if user_id != requester.user.to_string():
|
if validated_user_id != requester_user_id:
|
||||||
raise AuthError(403, "Invalid auth")
|
raise AuthError(403, "Invalid auth")
|
||||||
|
|
||||||
# Note that the access token has been validated.
|
# Note that the access token has been validated.
|
||||||
@ -402,13 +401,9 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
# if sso is enabled, allow the user to log in via SSO iff they have a mapping
|
# if sso is enabled, allow the user to log in via SSO iff they have a mapping
|
||||||
# from sso to mxid.
|
# from sso to mxid.
|
||||||
if self.hs.config.saml2.saml2_enabled or self.hs.config.oidc.oidc_enabled:
|
if await self.hs.get_sso_handler().get_identity_providers_for_user(
|
||||||
if await self.store.get_external_ids_by_user(user.to_string()):
|
user.to_string()
|
||||||
ui_auth_types.add(LoginType.SSO)
|
):
|
||||||
|
|
||||||
# Our CAS impl does not (yet) correctly register users in user_external_ids,
|
|
||||||
# so always offer that if it's available.
|
|
||||||
if self.hs.config.cas.cas_enabled:
|
|
||||||
ui_auth_types.add(LoginType.SSO)
|
ui_auth_types.add(LoginType.SSO)
|
||||||
|
|
||||||
return ui_auth_types
|
return ui_auth_types
|
||||||
@ -426,8 +421,8 @@ class AuthHandler(BaseHandler):
|
|||||||
flows: List[List[str]],
|
flows: List[List[str]],
|
||||||
request: SynapseRequest,
|
request: SynapseRequest,
|
||||||
clientdict: Dict[str, Any],
|
clientdict: Dict[str, Any],
|
||||||
clientip: str,
|
|
||||||
description: str,
|
description: str,
|
||||||
|
get_new_session_data: Optional[Callable[[], JsonDict]] = None,
|
||||||
) -> Tuple[dict, dict, str]:
|
) -> Tuple[dict, dict, str]:
|
||||||
"""
|
"""
|
||||||
Takes a dictionary sent by the client in the login / registration
|
Takes a dictionary sent by the client in the login / registration
|
||||||
@ -448,11 +443,16 @@ class AuthHandler(BaseHandler):
|
|||||||
clientdict: The dictionary from the client root level, not the
|
clientdict: The dictionary from the client root level, not the
|
||||||
'auth' key: this method prompts for auth if none is sent.
|
'auth' key: this method prompts for auth if none is sent.
|
||||||
|
|
||||||
clientip: The IP address of the client.
|
|
||||||
|
|
||||||
description: A human readable string to be displayed to the user that
|
description: A human readable string to be displayed to the user that
|
||||||
describes the operation happening on their account.
|
describes the operation happening on their account.
|
||||||
|
|
||||||
|
get_new_session_data:
|
||||||
|
an optional callback which will be called when starting a new session.
|
||||||
|
it should return data to be stored as part of the session.
|
||||||
|
|
||||||
|
The keys of the returned data should be entries in
|
||||||
|
UIAuthSessionDataConstants.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A tuple of (creds, params, session_id).
|
A tuple of (creds, params, session_id).
|
||||||
|
|
||||||
@ -480,10 +480,15 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
# If there's no session ID, create a new session.
|
# If there's no session ID, create a new session.
|
||||||
if not sid:
|
if not sid:
|
||||||
|
new_session_data = get_new_session_data() if get_new_session_data else {}
|
||||||
|
|
||||||
session = await self.store.create_ui_auth_session(
|
session = await self.store.create_ui_auth_session(
|
||||||
clientdict, uri, method, description
|
clientdict, uri, method, description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for k, v in new_session_data.items():
|
||||||
|
await self.set_session_data(session.session_id, k, v)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
session = await self.store.get_ui_auth_session(sid)
|
session = await self.store.get_ui_auth_session(sid)
|
||||||
@ -539,7 +544,8 @@ class AuthHandler(BaseHandler):
|
|||||||
# authentication flow.
|
# authentication flow.
|
||||||
await self.store.set_ui_auth_clientdict(sid, clientdict)
|
await self.store.set_ui_auth_clientdict(sid, clientdict)
|
||||||
|
|
||||||
user_agent = request.get_user_agent("")
|
user_agent = get_request_user_agent(request)
|
||||||
|
clientip = request.getClientIP()
|
||||||
|
|
||||||
await self.store.add_user_agent_ip_to_ui_auth_session(
|
await self.store.add_user_agent_ip_to_ui_auth_session(
|
||||||
session.session_id, user_agent, clientip
|
session.session_id, user_agent, clientip
|
||||||
@ -644,7 +650,8 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: The ID of this session as returned from check_auth
|
session_id: The ID of this session as returned from check_auth
|
||||||
key: The key to store the data under
|
key: The key to store the data under. An entry from
|
||||||
|
UIAuthSessionDataConstants.
|
||||||
value: The data to store
|
value: The data to store
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -660,7 +667,8 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id: The ID of this session as returned from check_auth
|
session_id: The ID of this session as returned from check_auth
|
||||||
key: The key to store the data under
|
key: The key the data was stored under. An entry from
|
||||||
|
UIAuthSessionDataConstants.
|
||||||
default: Value to return if the key has not been set
|
default: Value to return if the key has not been set
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -1334,12 +1342,12 @@ class AuthHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def start_sso_ui_auth(self, redirect_url: str, session_id: str) -> str:
|
async def start_sso_ui_auth(self, request: SynapseRequest, session_id: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get the HTML for the SSO redirect confirmation page.
|
Get the HTML for the SSO redirect confirmation page.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
redirect_url: The URL to redirect to the SSO provider.
|
request: The incoming HTTP request
|
||||||
session_id: The user interactive authentication session ID.
|
session_id: The user interactive authentication session ID.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -1349,31 +1357,39 @@ class AuthHandler(BaseHandler):
|
|||||||
session = await self.store.get_ui_auth_session(session_id)
|
session = await self.store.get_ui_auth_session(session_id)
|
||||||
except StoreError:
|
except StoreError:
|
||||||
raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
|
raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
|
||||||
|
|
||||||
|
user_id_to_verify = await self.get_session_data(
|
||||||
|
session_id, UIAuthSessionDataConstants.REQUEST_USER_ID
|
||||||
|
) # type: str
|
||||||
|
|
||||||
|
idps = await self.hs.get_sso_handler().get_identity_providers_for_user(
|
||||||
|
user_id_to_verify
|
||||||
|
)
|
||||||
|
|
||||||
|
if not idps:
|
||||||
|
# we checked that the user had some remote identities before offering an SSO
|
||||||
|
# flow, so either it's been deleted or the client has requested SSO despite
|
||||||
|
# it not being offered.
|
||||||
|
raise SynapseError(400, "User has no SSO identities")
|
||||||
|
|
||||||
|
# for now, just pick one
|
||||||
|
idp_id, sso_auth_provider = next(iter(idps.items()))
|
||||||
|
if len(idps) > 0:
|
||||||
|
logger.warning(
|
||||||
|
"User %r has previously logged in with multiple SSO IdPs; arbitrarily "
|
||||||
|
"picking %r",
|
||||||
|
user_id_to_verify,
|
||||||
|
idp_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect_url = await sso_auth_provider.handle_redirect_request(
|
||||||
|
request, None, session_id
|
||||||
|
)
|
||||||
|
|
||||||
return self._sso_auth_confirm_template.render(
|
return self._sso_auth_confirm_template.render(
|
||||||
description=session.description, redirect_url=redirect_url,
|
description=session.description, redirect_url=redirect_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def complete_sso_ui_auth(
|
|
||||||
self, registered_user_id: str, session_id: str, request: Request,
|
|
||||||
):
|
|
||||||
"""Having figured out a mxid for this user, complete the HTTP request
|
|
||||||
|
|
||||||
Args:
|
|
||||||
registered_user_id: The registered user ID to complete SSO login for.
|
|
||||||
session_id: The ID of the user-interactive auth session.
|
|
||||||
request: The request to complete.
|
|
||||||
"""
|
|
||||||
# Mark the stage of the authentication as successful.
|
|
||||||
# Save the user who authenticated with SSO, this will be used to ensure
|
|
||||||
# that the account be modified is also the person who logged in.
|
|
||||||
await self.store.mark_ui_auth_stage_complete(
|
|
||||||
session_id, LoginType.SSO, registered_user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Render the HTML and return.
|
|
||||||
html = self._sso_auth_success_template
|
|
||||||
respond_with_html(request, 200, html)
|
|
||||||
|
|
||||||
async def complete_sso_login(
|
async def complete_sso_login(
|
||||||
self,
|
self,
|
||||||
registered_user_id: str,
|
registered_user_id: str,
|
||||||
@ -1488,8 +1504,8 @@ class AuthHandler(BaseHandler):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def add_query_param_to_url(url: str, param_name: str, param: Any):
|
def add_query_param_to_url(url: str, param_name: str, param: Any):
|
||||||
url_parts = list(urllib.parse.urlparse(url))
|
url_parts = list(urllib.parse.urlparse(url))
|
||||||
query = dict(urllib.parse.parse_qsl(url_parts[4]))
|
query = urllib.parse.parse_qsl(url_parts[4], keep_blank_values=True)
|
||||||
query.update({param_name: param})
|
query.append((param_name, param))
|
||||||
url_parts[4] = urllib.parse.urlencode(query)
|
url_parts[4] = urllib.parse.urlencode(query)
|
||||||
return urllib.parse.urlunparse(url_parts)
|
return urllib.parse.urlunparse(url_parts)
|
||||||
|
|
||||||
|
@ -80,6 +80,10 @@ class CasHandler:
|
|||||||
# user-facing name of this auth provider
|
# user-facing name of this auth provider
|
||||||
self.idp_name = "CAS"
|
self.idp_name = "CAS"
|
||||||
|
|
||||||
|
# we do not currently support icons for CAS auth, but this is required by
|
||||||
|
# the SsoIdentityProvider protocol type.
|
||||||
|
self.idp_icon = None
|
||||||
|
|
||||||
self._sso_handler = hs.get_sso_handler()
|
self._sso_handler = hs.get_sso_handler()
|
||||||
|
|
||||||
self._sso_handler.register_identity_provider(self)
|
self._sso_handler.register_identity_provider(self)
|
||||||
|
@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Optional
|
|||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||||
from synapse.types import UserID, create_requester
|
from synapse.types import Requester, UserID, create_requester
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@ -38,6 +38,7 @@ class DeactivateAccountHandler(BaseHandler):
|
|||||||
self._device_handler = hs.get_device_handler()
|
self._device_handler = hs.get_device_handler()
|
||||||
self._room_member_handler = hs.get_room_member_handler()
|
self._room_member_handler = hs.get_room_member_handler()
|
||||||
self._identity_handler = hs.get_identity_handler()
|
self._identity_handler = hs.get_identity_handler()
|
||||||
|
self._profile_handler = hs.get_profile_handler()
|
||||||
self.user_directory_handler = hs.get_user_directory_handler()
|
self.user_directory_handler = hs.get_user_directory_handler()
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
|
|
||||||
@ -52,16 +53,23 @@ class DeactivateAccountHandler(BaseHandler):
|
|||||||
self._account_validity_enabled = hs.config.account_validity.enabled
|
self._account_validity_enabled = hs.config.account_validity.enabled
|
||||||
|
|
||||||
async def deactivate_account(
|
async def deactivate_account(
|
||||||
self, user_id: str, erase_data: bool, id_server: Optional[str] = None
|
self,
|
||||||
|
user_id: str,
|
||||||
|
erase_data: bool,
|
||||||
|
requester: Requester,
|
||||||
|
id_server: Optional[str] = None,
|
||||||
|
by_admin: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Deactivate a user's account
|
"""Deactivate a user's account
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: ID of user to be deactivated
|
user_id: ID of user to be deactivated
|
||||||
erase_data: whether to GDPR-erase the user's data
|
erase_data: whether to GDPR-erase the user's data
|
||||||
|
requester: The user attempting to make this change.
|
||||||
id_server: Use the given identity server when unbinding
|
id_server: Use the given identity server when unbinding
|
||||||
any threepids. If None then will attempt to unbind using the
|
any threepids. If None then will attempt to unbind using the
|
||||||
identity server specified when binding (if known).
|
identity server specified when binding (if known).
|
||||||
|
by_admin: Whether this change was made by an administrator.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if identity server supports removing threepids, otherwise False.
|
True if identity server supports removing threepids, otherwise False.
|
||||||
@ -121,6 +129,12 @@ class DeactivateAccountHandler(BaseHandler):
|
|||||||
|
|
||||||
# Mark the user as erased, if they asked for that
|
# Mark the user as erased, if they asked for that
|
||||||
if erase_data:
|
if erase_data:
|
||||||
|
user = UserID.from_string(user_id)
|
||||||
|
# Remove avatar URL from this user
|
||||||
|
await self._profile_handler.set_avatar_url(user, requester, "", by_admin)
|
||||||
|
# Remove displayname from this user
|
||||||
|
await self._profile_handler.set_displayname(user, requester, "", by_admin)
|
||||||
|
|
||||||
logger.info("Marking %s as erased", user_id)
|
logger.info("Marking %s as erased", user_id)
|
||||||
await self.store.mark_user_erased(user_id)
|
await self.store.mark_user_erased(user_id)
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ class DeviceMessageHandler:
|
|||||||
await self.store.mark_remote_user_device_cache_as_stale(sender_user_id)
|
await self.store.mark_remote_user_device_cache_as_stale(sender_user_id)
|
||||||
|
|
||||||
# Immediately attempt a resync in the background
|
# Immediately attempt a resync in the background
|
||||||
run_in_background(self._user_device_resync, sender_user_id)
|
run_in_background(self._user_device_resync, user_id=sender_user_id)
|
||||||
|
|
||||||
async def send_device_message(
|
async def send_device_message(
|
||||||
self,
|
self,
|
||||||
|
@ -476,8 +476,6 @@ class IdentityHandler(BaseHandler):
|
|||||||
except RequestTimedOutError:
|
except RequestTimedOutError:
|
||||||
raise SynapseError(500, "Timed out contacting identity server")
|
raise SynapseError(500, "Timed out contacting identity server")
|
||||||
|
|
||||||
assert self.hs.config.public_baseurl
|
|
||||||
|
|
||||||
# we need to tell the client to send the token back to us, since it doesn't
|
# we need to tell the client to send the token back to us, since it doesn't
|
||||||
# otherwise know where to send it, so add submit_url response parameter
|
# otherwise know where to send it, so add submit_url response parameter
|
||||||
# (see also MSC2078)
|
# (see also MSC2078)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Dict, Generic, List, Optional, Tuple, TypeVar
|
from typing import TYPE_CHECKING, Dict, Generic, List, Optional, TypeVar
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
@ -35,7 +35,7 @@ from typing_extensions import TypedDict
|
|||||||
from twisted.web.client import readBody
|
from twisted.web.client import readBody
|
||||||
|
|
||||||
from synapse.config import ConfigError
|
from synapse.config import ConfigError
|
||||||
from synapse.handlers._base import BaseHandler
|
from synapse.config.oidc_config import OidcProviderConfig
|
||||||
from synapse.handlers.sso import MappingException, UserAttributes
|
from synapse.handlers.sso import MappingException, UserAttributes
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging.context import make_deferred_yieldable
|
from synapse.logging.context import make_deferred_yieldable
|
||||||
@ -71,6 +71,144 @@ JWK = Dict[str, str]
|
|||||||
JWKS = TypedDict("JWKS", {"keys": List[JWK]})
|
JWKS = TypedDict("JWKS", {"keys": List[JWK]})
|
||||||
|
|
||||||
|
|
||||||
|
class OidcHandler:
|
||||||
|
"""Handles requests related to the OpenID Connect login flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._sso_handler = hs.get_sso_handler()
|
||||||
|
|
||||||
|
provider_confs = hs.config.oidc.oidc_providers
|
||||||
|
# we should not have been instantiated if there is no configured provider.
|
||||||
|
assert provider_confs
|
||||||
|
|
||||||
|
self._token_generator = OidcSessionTokenGenerator(hs)
|
||||||
|
self._providers = {
|
||||||
|
p.idp_id: OidcProvider(hs, self._token_generator, p) for p in provider_confs
|
||||||
|
} # type: Dict[str, OidcProvider]
|
||||||
|
|
||||||
|
async def load_metadata(self) -> None:
|
||||||
|
"""Validate the config and load the metadata from the remote endpoint.
|
||||||
|
|
||||||
|
Called at startup to ensure we have everything we need.
|
||||||
|
"""
|
||||||
|
for idp_id, p in self._providers.items():
|
||||||
|
try:
|
||||||
|
await p.load_metadata()
|
||||||
|
await p.load_jwks()
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(
|
||||||
|
"Error while initialising OIDC provider %r" % (idp_id,)
|
||||||
|
) from e
|
||||||
|
|
||||||
|
async def handle_oidc_callback(self, request: SynapseRequest) -> None:
|
||||||
|
"""Handle an incoming request to /_synapse/oidc/callback
|
||||||
|
|
||||||
|
Since we might want to display OIDC-related errors in a user-friendly
|
||||||
|
way, we don't raise SynapseError from here. Instead, we call
|
||||||
|
``self._sso_handler.render_error`` which displays an HTML page for the error.
|
||||||
|
|
||||||
|
Most of the OpenID Connect logic happens here:
|
||||||
|
|
||||||
|
- first, we check if there was any error returned by the provider and
|
||||||
|
display it
|
||||||
|
- then we fetch the session cookie, decode and verify it
|
||||||
|
- the ``state`` query parameter should match with the one stored in the
|
||||||
|
session cookie
|
||||||
|
|
||||||
|
Once we know the session is legit, we then delegate to the OIDC Provider
|
||||||
|
implementation, which will exchange the code with the provider and complete the
|
||||||
|
login/authentication.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: the incoming request from the browser.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The provider might redirect with an error.
|
||||||
|
# In that case, just display it as-is.
|
||||||
|
if b"error" in request.args:
|
||||||
|
# error response from the auth server. see:
|
||||||
|
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
||||||
|
# https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||||
|
error = request.args[b"error"][0].decode()
|
||||||
|
description = request.args.get(b"error_description", [b""])[0].decode()
|
||||||
|
|
||||||
|
# Most of the errors returned by the provider could be due by
|
||||||
|
# either the provider misbehaving or Synapse being misconfigured.
|
||||||
|
# The only exception of that is "access_denied", where the user
|
||||||
|
# probably cancelled the login flow. In other cases, log those errors.
|
||||||
|
if error != "access_denied":
|
||||||
|
logger.error("Error from the OIDC provider: %s %s", error, description)
|
||||||
|
|
||||||
|
self._sso_handler.render_error(request, error, description)
|
||||||
|
return
|
||||||
|
|
||||||
|
# otherwise, it is presumably a successful response. see:
|
||||||
|
# https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||||
|
|
||||||
|
# Fetch the session cookie
|
||||||
|
session = request.getCookie(SESSION_COOKIE_NAME) # type: Optional[bytes]
|
||||||
|
if session is None:
|
||||||
|
logger.info("No session cookie found")
|
||||||
|
self._sso_handler.render_error(
|
||||||
|
request, "missing_session", "No session cookie found"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove the cookie. There is a good chance that if the callback failed
|
||||||
|
# once, it will fail next time and the code will already be exchanged.
|
||||||
|
# Removing it early avoids spamming the provider with token requests.
|
||||||
|
request.addCookie(
|
||||||
|
SESSION_COOKIE_NAME,
|
||||||
|
b"",
|
||||||
|
path="/_synapse/oidc",
|
||||||
|
expires="Thu, Jan 01 1970 00:00:00 UTC",
|
||||||
|
httpOnly=True,
|
||||||
|
sameSite="lax",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for the state query parameter
|
||||||
|
if b"state" not in request.args:
|
||||||
|
logger.info("State parameter is missing")
|
||||||
|
self._sso_handler.render_error(
|
||||||
|
request, "invalid_request", "State parameter is missing"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
state = request.args[b"state"][0].decode()
|
||||||
|
|
||||||
|
# Deserialize the session token and verify it.
|
||||||
|
try:
|
||||||
|
session_data = self._token_generator.verify_oidc_session_token(
|
||||||
|
session, state
|
||||||
|
)
|
||||||
|
except (MacaroonDeserializationException, ValueError) as e:
|
||||||
|
logger.exception("Invalid session")
|
||||||
|
self._sso_handler.render_error(request, "invalid_session", str(e))
|
||||||
|
return
|
||||||
|
except MacaroonInvalidSignatureException as e:
|
||||||
|
logger.exception("Could not verify session")
|
||||||
|
self._sso_handler.render_error(request, "mismatching_session", str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
oidc_provider = self._providers.get(session_data.idp_id)
|
||||||
|
if not oidc_provider:
|
||||||
|
logger.error("OIDC session uses unknown IdP %r", oidc_provider)
|
||||||
|
self._sso_handler.render_error(request, "unknown_idp", "Unknown IdP")
|
||||||
|
return
|
||||||
|
|
||||||
|
if b"code" not in request.args:
|
||||||
|
logger.info("Code parameter is missing")
|
||||||
|
self._sso_handler.render_error(
|
||||||
|
request, "invalid_request", "Code parameter is missing"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
code = request.args[b"code"][0].decode()
|
||||||
|
|
||||||
|
await oidc_provider.handle_oidc_callback(request, session_data, code)
|
||||||
|
|
||||||
|
|
||||||
class OidcError(Exception):
|
class OidcError(Exception):
|
||||||
"""Used to catch errors when calling the token_endpoint
|
"""Used to catch errors when calling the token_endpoint
|
||||||
"""
|
"""
|
||||||
@ -85,44 +223,56 @@ class OidcError(Exception):
|
|||||||
return self.error
|
return self.error
|
||||||
|
|
||||||
|
|
||||||
class OidcHandler(BaseHandler):
|
class OidcProvider:
|
||||||
"""Handles requests related to the OpenID Connect login flow.
|
"""Wraps the config for a single OIDC IdentityProvider
|
||||||
|
|
||||||
|
Provides methods for handling redirect requests and callbacks via that particular
|
||||||
|
IdP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(
|
||||||
super().__init__(hs)
|
self,
|
||||||
|
hs: "HomeServer",
|
||||||
|
token_generator: "OidcSessionTokenGenerator",
|
||||||
|
provider: OidcProviderConfig,
|
||||||
|
):
|
||||||
|
self._store = hs.get_datastore()
|
||||||
|
|
||||||
|
self._token_generator = token_generator
|
||||||
|
|
||||||
self._callback_url = hs.config.oidc_callback_url # type: str
|
self._callback_url = hs.config.oidc_callback_url # type: str
|
||||||
self._scopes = hs.config.oidc_scopes # type: List[str]
|
|
||||||
self._user_profile_method = hs.config.oidc_user_profile_method # type: str
|
self._scopes = provider.scopes
|
||||||
|
self._user_profile_method = provider.user_profile_method
|
||||||
self._client_auth = ClientAuth(
|
self._client_auth = ClientAuth(
|
||||||
hs.config.oidc_client_id,
|
provider.client_id, provider.client_secret, provider.client_auth_method,
|
||||||
hs.config.oidc_client_secret,
|
|
||||||
hs.config.oidc_client_auth_method,
|
|
||||||
) # type: ClientAuth
|
) # type: ClientAuth
|
||||||
self._client_auth_method = hs.config.oidc_client_auth_method # type: str
|
self._client_auth_method = provider.client_auth_method
|
||||||
self._provider_metadata = OpenIDProviderMetadata(
|
self._provider_metadata = OpenIDProviderMetadata(
|
||||||
issuer=hs.config.oidc_issuer,
|
issuer=provider.issuer,
|
||||||
authorization_endpoint=hs.config.oidc_authorization_endpoint,
|
authorization_endpoint=provider.authorization_endpoint,
|
||||||
token_endpoint=hs.config.oidc_token_endpoint,
|
token_endpoint=provider.token_endpoint,
|
||||||
userinfo_endpoint=hs.config.oidc_userinfo_endpoint,
|
userinfo_endpoint=provider.userinfo_endpoint,
|
||||||
jwks_uri=hs.config.oidc_jwks_uri,
|
jwks_uri=provider.jwks_uri,
|
||||||
) # type: OpenIDProviderMetadata
|
) # type: OpenIDProviderMetadata
|
||||||
self._provider_needs_discovery = hs.config.oidc_discover # type: bool
|
self._provider_needs_discovery = provider.discover
|
||||||
self._user_mapping_provider = hs.config.oidc_user_mapping_provider_class(
|
self._user_mapping_provider = provider.user_mapping_provider_class(
|
||||||
hs.config.oidc_user_mapping_provider_config
|
provider.user_mapping_provider_config
|
||||||
) # type: OidcMappingProvider
|
)
|
||||||
self._skip_verification = hs.config.oidc_skip_verification # type: bool
|
self._skip_verification = provider.skip_verification
|
||||||
self._allow_existing_users = hs.config.oidc_allow_existing_users # type: bool
|
self._allow_existing_users = provider.allow_existing_users
|
||||||
|
|
||||||
self._http_client = hs.get_proxied_http_client()
|
self._http_client = hs.get_proxied_http_client()
|
||||||
self._server_name = hs.config.server_name # type: str
|
self._server_name = hs.config.server_name # type: str
|
||||||
self._macaroon_secret_key = hs.config.macaroon_secret_key
|
|
||||||
|
|
||||||
# identifier for the external_ids table
|
# identifier for the external_ids table
|
||||||
self.idp_id = "oidc"
|
self.idp_id = provider.idp_id
|
||||||
|
|
||||||
# user-facing name of this auth provider
|
# user-facing name of this auth provider
|
||||||
self.idp_name = "OIDC"
|
self.idp_name = provider.idp_name
|
||||||
|
|
||||||
|
# MXC URI for icon for this auth provider
|
||||||
|
self.idp_icon = provider.idp_icon
|
||||||
|
|
||||||
self._sso_handler = hs.get_sso_handler()
|
self._sso_handler = hs.get_sso_handler()
|
||||||
|
|
||||||
@ -519,11 +669,14 @@ class OidcHandler(BaseHandler):
|
|||||||
if not client_redirect_url:
|
if not client_redirect_url:
|
||||||
client_redirect_url = b""
|
client_redirect_url = b""
|
||||||
|
|
||||||
cookie = self._generate_oidc_session_token(
|
cookie = self._token_generator.generate_oidc_session_token(
|
||||||
state=state,
|
state=state,
|
||||||
nonce=nonce,
|
session_data=OidcSessionData(
|
||||||
client_redirect_url=client_redirect_url.decode(),
|
idp_id=self.idp_id,
|
||||||
ui_auth_session_id=ui_auth_session_id,
|
nonce=nonce,
|
||||||
|
client_redirect_url=client_redirect_url.decode(),
|
||||||
|
ui_auth_session_id=ui_auth_session_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
request.addCookie(
|
request.addCookie(
|
||||||
SESSION_COOKIE_NAME,
|
SESSION_COOKIE_NAME,
|
||||||
@ -546,22 +699,16 @@ class OidcHandler(BaseHandler):
|
|||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_oidc_callback(self, request: SynapseRequest) -> None:
|
async def handle_oidc_callback(
|
||||||
|
self, request: SynapseRequest, session_data: "OidcSessionData", code: str
|
||||||
|
) -> None:
|
||||||
"""Handle an incoming request to /_synapse/oidc/callback
|
"""Handle an incoming request to /_synapse/oidc/callback
|
||||||
|
|
||||||
Since we might want to display OIDC-related errors in a user-friendly
|
By this time we have already validated the session on the synapse side, and
|
||||||
way, we don't raise SynapseError from here. Instead, we call
|
now need to do the provider-specific operations. This includes:
|
||||||
``self._sso_handler.render_error`` which displays an HTML page for the error.
|
|
||||||
|
|
||||||
Most of the OpenID Connect logic happens here:
|
- exchange the code with the provider using the ``token_endpoint`` (see
|
||||||
|
``_exchange_code``)
|
||||||
- first, we check if there was any error returned by the provider and
|
|
||||||
display it
|
|
||||||
- then we fetch the session cookie, decode and verify it
|
|
||||||
- the ``state`` query parameter should match with the one stored in the
|
|
||||||
session cookie
|
|
||||||
- once we known this session is legit, exchange the code with the
|
|
||||||
provider using the ``token_endpoint`` (see ``_exchange_code``)
|
|
||||||
- once we have the token, use it to either extract the UserInfo from
|
- once we have the token, use it to either extract the UserInfo from
|
||||||
the ``id_token`` (``_parse_id_token``), or use the ``access_token``
|
the ``id_token`` (``_parse_id_token``), or use the ``access_token``
|
||||||
to fetch UserInfo from the ``userinfo_endpoint``
|
to fetch UserInfo from the ``userinfo_endpoint``
|
||||||
@ -571,88 +718,12 @@ class OidcHandler(BaseHandler):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: the incoming request from the browser.
|
request: the incoming request from the browser.
|
||||||
|
session_data: the session data, extracted from our cookie
|
||||||
|
code: The authorization code we got from the callback.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The provider might redirect with an error.
|
|
||||||
# In that case, just display it as-is.
|
|
||||||
if b"error" in request.args:
|
|
||||||
# error response from the auth server. see:
|
|
||||||
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
|
||||||
# https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
||||||
error = request.args[b"error"][0].decode()
|
|
||||||
description = request.args.get(b"error_description", [b""])[0].decode()
|
|
||||||
|
|
||||||
# Most of the errors returned by the provider could be due by
|
|
||||||
# either the provider misbehaving or Synapse being misconfigured.
|
|
||||||
# The only exception of that is "access_denied", where the user
|
|
||||||
# probably cancelled the login flow. In other cases, log those errors.
|
|
||||||
if error != "access_denied":
|
|
||||||
logger.error("Error from the OIDC provider: %s %s", error, description)
|
|
||||||
|
|
||||||
self._sso_handler.render_error(request, error, description)
|
|
||||||
return
|
|
||||||
|
|
||||||
# otherwise, it is presumably a successful response. see:
|
|
||||||
# https://tools.ietf.org/html/rfc6749#section-4.1.2
|
|
||||||
|
|
||||||
# Fetch the session cookie
|
|
||||||
session = request.getCookie(SESSION_COOKIE_NAME) # type: Optional[bytes]
|
|
||||||
if session is None:
|
|
||||||
logger.info("No session cookie found")
|
|
||||||
self._sso_handler.render_error(
|
|
||||||
request, "missing_session", "No session cookie found"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Remove the cookie. There is a good chance that if the callback failed
|
|
||||||
# once, it will fail next time and the code will already be exchanged.
|
|
||||||
# Removing it early avoids spamming the provider with token requests.
|
|
||||||
request.addCookie(
|
|
||||||
SESSION_COOKIE_NAME,
|
|
||||||
b"",
|
|
||||||
path="/_synapse/oidc",
|
|
||||||
expires="Thu, Jan 01 1970 00:00:00 UTC",
|
|
||||||
httpOnly=True,
|
|
||||||
sameSite="lax",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for the state query parameter
|
|
||||||
if b"state" not in request.args:
|
|
||||||
logger.info("State parameter is missing")
|
|
||||||
self._sso_handler.render_error(
|
|
||||||
request, "invalid_request", "State parameter is missing"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
state = request.args[b"state"][0].decode()
|
|
||||||
|
|
||||||
# Deserialize the session token and verify it.
|
|
||||||
try:
|
|
||||||
(
|
|
||||||
nonce,
|
|
||||||
client_redirect_url,
|
|
||||||
ui_auth_session_id,
|
|
||||||
) = self._verify_oidc_session_token(session, state)
|
|
||||||
except MacaroonDeserializationException as e:
|
|
||||||
logger.exception("Invalid session")
|
|
||||||
self._sso_handler.render_error(request, "invalid_session", str(e))
|
|
||||||
return
|
|
||||||
except MacaroonInvalidSignatureException as e:
|
|
||||||
logger.exception("Could not verify session")
|
|
||||||
self._sso_handler.render_error(request, "mismatching_session", str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Exchange the code with the provider
|
# Exchange the code with the provider
|
||||||
if b"code" not in request.args:
|
|
||||||
logger.info("Code parameter is missing")
|
|
||||||
self._sso_handler.render_error(
|
|
||||||
request, "invalid_request", "Code parameter is missing"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug("Exchanging code")
|
|
||||||
code = request.args[b"code"][0].decode()
|
|
||||||
try:
|
try:
|
||||||
|
logger.debug("Exchanging code")
|
||||||
token = await self._exchange_code(code)
|
token = await self._exchange_code(code)
|
||||||
except OidcError as e:
|
except OidcError as e:
|
||||||
logger.exception("Could not exchange code")
|
logger.exception("Could not exchange code")
|
||||||
@ -674,14 +745,14 @@ class OidcHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
logger.debug("Extracting userinfo from id_token")
|
logger.debug("Extracting userinfo from id_token")
|
||||||
try:
|
try:
|
||||||
userinfo = await self._parse_id_token(token, nonce=nonce)
|
userinfo = await self._parse_id_token(token, nonce=session_data.nonce)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Invalid id_token")
|
logger.exception("Invalid id_token")
|
||||||
self._sso_handler.render_error(request, "invalid_token", str(e))
|
self._sso_handler.render_error(request, "invalid_token", str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
# first check if we're doing a UIA
|
# first check if we're doing a UIA
|
||||||
if ui_auth_session_id:
|
if session_data.ui_auth_session_id:
|
||||||
try:
|
try:
|
||||||
remote_user_id = self._remote_id_from_userinfo(userinfo)
|
remote_user_id = self._remote_id_from_userinfo(userinfo)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -690,7 +761,7 @@ class OidcHandler(BaseHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
return await self._sso_handler.complete_sso_ui_auth_request(
|
return await self._sso_handler.complete_sso_ui_auth_request(
|
||||||
self.idp_id, remote_user_id, ui_auth_session_id, request
|
self.idp_id, remote_user_id, session_data.ui_auth_session_id, request
|
||||||
)
|
)
|
||||||
|
|
||||||
# otherwise, it's a login
|
# otherwise, it's a login
|
||||||
@ -698,133 +769,12 @@ class OidcHandler(BaseHandler):
|
|||||||
# Call the mapper to register/login the user
|
# Call the mapper to register/login the user
|
||||||
try:
|
try:
|
||||||
await self._complete_oidc_login(
|
await self._complete_oidc_login(
|
||||||
userinfo, token, request, client_redirect_url
|
userinfo, token, request, session_data.client_redirect_url
|
||||||
)
|
)
|
||||||
except MappingException as e:
|
except MappingException as e:
|
||||||
logger.exception("Could not map user")
|
logger.exception("Could not map user")
|
||||||
self._sso_handler.render_error(request, "mapping_error", str(e))
|
self._sso_handler.render_error(request, "mapping_error", str(e))
|
||||||
|
|
||||||
def _generate_oidc_session_token(
|
|
||||||
self,
|
|
||||||
state: str,
|
|
||||||
nonce: str,
|
|
||||||
client_redirect_url: str,
|
|
||||||
ui_auth_session_id: Optional[str],
|
|
||||||
duration_in_ms: int = (60 * 60 * 1000),
|
|
||||||
) -> str:
|
|
||||||
"""Generates a signed token storing data about an OIDC session.
|
|
||||||
|
|
||||||
When Synapse initiates an authorization flow, it creates a random state
|
|
||||||
and a random nonce. Those parameters are given to the provider and
|
|
||||||
should be verified when the client comes back from the provider.
|
|
||||||
It is also used to store the client_redirect_url, which is used to
|
|
||||||
complete the SSO login flow.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: The ``state`` parameter passed to the OIDC provider.
|
|
||||||
nonce: The ``nonce`` parameter passed to the OIDC provider.
|
|
||||||
client_redirect_url: The URL the client gave when it initiated the
|
|
||||||
flow.
|
|
||||||
ui_auth_session_id: The session ID of the ongoing UI Auth (or
|
|
||||||
None if this is a login).
|
|
||||||
duration_in_ms: An optional duration for the token in milliseconds.
|
|
||||||
Defaults to an hour.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A signed macaroon token with the session information.
|
|
||||||
"""
|
|
||||||
macaroon = pymacaroons.Macaroon(
|
|
||||||
location=self._server_name, identifier="key", key=self._macaroon_secret_key,
|
|
||||||
)
|
|
||||||
macaroon.add_first_party_caveat("gen = 1")
|
|
||||||
macaroon.add_first_party_caveat("type = session")
|
|
||||||
macaroon.add_first_party_caveat("state = %s" % (state,))
|
|
||||||
macaroon.add_first_party_caveat("nonce = %s" % (nonce,))
|
|
||||||
macaroon.add_first_party_caveat(
|
|
||||||
"client_redirect_url = %s" % (client_redirect_url,)
|
|
||||||
)
|
|
||||||
if ui_auth_session_id:
|
|
||||||
macaroon.add_first_party_caveat(
|
|
||||||
"ui_auth_session_id = %s" % (ui_auth_session_id,)
|
|
||||||
)
|
|
||||||
now = self.clock.time_msec()
|
|
||||||
expiry = now + duration_in_ms
|
|
||||||
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
|
||||||
|
|
||||||
return macaroon.serialize()
|
|
||||||
|
|
||||||
def _verify_oidc_session_token(
|
|
||||||
self, session: bytes, state: str
|
|
||||||
) -> Tuple[str, str, Optional[str]]:
|
|
||||||
"""Verifies and extract an OIDC session token.
|
|
||||||
|
|
||||||
This verifies that a given session token was issued by this homeserver
|
|
||||||
and extract the nonce and client_redirect_url caveats.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session: The session token to verify
|
|
||||||
state: The state the OIDC provider gave back
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The nonce, client_redirect_url, and ui_auth_session_id for this session
|
|
||||||
"""
|
|
||||||
macaroon = pymacaroons.Macaroon.deserialize(session)
|
|
||||||
|
|
||||||
v = pymacaroons.Verifier()
|
|
||||||
v.satisfy_exact("gen = 1")
|
|
||||||
v.satisfy_exact("type = session")
|
|
||||||
v.satisfy_exact("state = %s" % (state,))
|
|
||||||
v.satisfy_general(lambda c: c.startswith("nonce = "))
|
|
||||||
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
|
|
||||||
# Sometimes there's a UI auth session ID, it seems to be OK to attempt
|
|
||||||
# to always satisfy this.
|
|
||||||
v.satisfy_general(lambda c: c.startswith("ui_auth_session_id = "))
|
|
||||||
v.satisfy_general(self._verify_expiry)
|
|
||||||
|
|
||||||
v.verify(macaroon, self._macaroon_secret_key)
|
|
||||||
|
|
||||||
# Extract the `nonce`, `client_redirect_url`, and maybe the
|
|
||||||
# `ui_auth_session_id` from the token.
|
|
||||||
nonce = self._get_value_from_macaroon(macaroon, "nonce")
|
|
||||||
client_redirect_url = self._get_value_from_macaroon(
|
|
||||||
macaroon, "client_redirect_url"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
ui_auth_session_id = self._get_value_from_macaroon(
|
|
||||||
macaroon, "ui_auth_session_id"
|
|
||||||
) # type: Optional[str]
|
|
||||||
except ValueError:
|
|
||||||
ui_auth_session_id = None
|
|
||||||
|
|
||||||
return nonce, client_redirect_url, ui_auth_session_id
|
|
||||||
|
|
||||||
def _get_value_from_macaroon(self, macaroon: pymacaroons.Macaroon, key: str) -> str:
|
|
||||||
"""Extracts a caveat value from a macaroon token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
macaroon: the token
|
|
||||||
key: the key of the caveat to extract
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The extracted value
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: if the caveat was not in the macaroon
|
|
||||||
"""
|
|
||||||
prefix = key + " = "
|
|
||||||
for caveat in macaroon.caveats:
|
|
||||||
if caveat.caveat_id.startswith(prefix):
|
|
||||||
return caveat.caveat_id[len(prefix) :]
|
|
||||||
raise ValueError("No %s caveat in macaroon" % (key,))
|
|
||||||
|
|
||||||
def _verify_expiry(self, caveat: str) -> bool:
|
|
||||||
prefix = "time < "
|
|
||||||
if not caveat.startswith(prefix):
|
|
||||||
return False
|
|
||||||
expiry = int(caveat[len(prefix) :])
|
|
||||||
now = self.clock.time_msec()
|
|
||||||
return now < expiry
|
|
||||||
|
|
||||||
async def _complete_oidc_login(
|
async def _complete_oidc_login(
|
||||||
self,
|
self,
|
||||||
userinfo: UserInfo,
|
userinfo: UserInfo,
|
||||||
@ -901,8 +851,8 @@ class OidcHandler(BaseHandler):
|
|||||||
# and attempt to match it.
|
# and attempt to match it.
|
||||||
attributes = await oidc_response_to_user_attributes(failures=0)
|
attributes = await oidc_response_to_user_attributes(failures=0)
|
||||||
|
|
||||||
user_id = UserID(attributes.localpart, self.server_name).to_string()
|
user_id = UserID(attributes.localpart, self._server_name).to_string()
|
||||||
users = await self.store.get_users_by_id_case_insensitive(user_id)
|
users = await self._store.get_users_by_id_case_insensitive(user_id)
|
||||||
if users:
|
if users:
|
||||||
# If an existing matrix ID is returned, then use it.
|
# If an existing matrix ID is returned, then use it.
|
||||||
if len(users) == 1:
|
if len(users) == 1:
|
||||||
@ -954,6 +904,157 @@ class OidcHandler(BaseHandler):
|
|||||||
return str(remote_user_id)
|
return str(remote_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class OidcSessionTokenGenerator:
|
||||||
|
"""Methods for generating and checking OIDC Session cookies."""
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._clock = hs.get_clock()
|
||||||
|
self._server_name = hs.hostname
|
||||||
|
self._macaroon_secret_key = hs.config.key.macaroon_secret_key
|
||||||
|
|
||||||
|
def generate_oidc_session_token(
|
||||||
|
self,
|
||||||
|
state: str,
|
||||||
|
session_data: "OidcSessionData",
|
||||||
|
duration_in_ms: int = (60 * 60 * 1000),
|
||||||
|
) -> str:
|
||||||
|
"""Generates a signed token storing data about an OIDC session.
|
||||||
|
|
||||||
|
When Synapse initiates an authorization flow, it creates a random state
|
||||||
|
and a random nonce. Those parameters are given to the provider and
|
||||||
|
should be verified when the client comes back from the provider.
|
||||||
|
It is also used to store the client_redirect_url, which is used to
|
||||||
|
complete the SSO login flow.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: The ``state`` parameter passed to the OIDC provider.
|
||||||
|
session_data: data to include in the session token.
|
||||||
|
duration_in_ms: An optional duration for the token in milliseconds.
|
||||||
|
Defaults to an hour.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A signed macaroon token with the session information.
|
||||||
|
"""
|
||||||
|
macaroon = pymacaroons.Macaroon(
|
||||||
|
location=self._server_name, identifier="key", key=self._macaroon_secret_key,
|
||||||
|
)
|
||||||
|
macaroon.add_first_party_caveat("gen = 1")
|
||||||
|
macaroon.add_first_party_caveat("type = session")
|
||||||
|
macaroon.add_first_party_caveat("state = %s" % (state,))
|
||||||
|
macaroon.add_first_party_caveat("idp_id = %s" % (session_data.idp_id,))
|
||||||
|
macaroon.add_first_party_caveat("nonce = %s" % (session_data.nonce,))
|
||||||
|
macaroon.add_first_party_caveat(
|
||||||
|
"client_redirect_url = %s" % (session_data.client_redirect_url,)
|
||||||
|
)
|
||||||
|
if session_data.ui_auth_session_id:
|
||||||
|
macaroon.add_first_party_caveat(
|
||||||
|
"ui_auth_session_id = %s" % (session_data.ui_auth_session_id,)
|
||||||
|
)
|
||||||
|
now = self._clock.time_msec()
|
||||||
|
expiry = now + duration_in_ms
|
||||||
|
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||||
|
|
||||||
|
return macaroon.serialize()
|
||||||
|
|
||||||
|
def verify_oidc_session_token(
|
||||||
|
self, session: bytes, state: str
|
||||||
|
) -> "OidcSessionData":
|
||||||
|
"""Verifies and extract an OIDC session token.
|
||||||
|
|
||||||
|
This verifies that a given session token was issued by this homeserver
|
||||||
|
and extract the nonce and client_redirect_url caveats.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The session token to verify
|
||||||
|
state: The state the OIDC provider gave back
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The data extracted from the session cookie
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError if an expected caveat is missing from the macaroon.
|
||||||
|
"""
|
||||||
|
macaroon = pymacaroons.Macaroon.deserialize(session)
|
||||||
|
|
||||||
|
v = pymacaroons.Verifier()
|
||||||
|
v.satisfy_exact("gen = 1")
|
||||||
|
v.satisfy_exact("type = session")
|
||||||
|
v.satisfy_exact("state = %s" % (state,))
|
||||||
|
v.satisfy_general(lambda c: c.startswith("nonce = "))
|
||||||
|
v.satisfy_general(lambda c: c.startswith("idp_id = "))
|
||||||
|
v.satisfy_general(lambda c: c.startswith("client_redirect_url = "))
|
||||||
|
# Sometimes there's a UI auth session ID, it seems to be OK to attempt
|
||||||
|
# to always satisfy this.
|
||||||
|
v.satisfy_general(lambda c: c.startswith("ui_auth_session_id = "))
|
||||||
|
v.satisfy_general(self._verify_expiry)
|
||||||
|
|
||||||
|
v.verify(macaroon, self._macaroon_secret_key)
|
||||||
|
|
||||||
|
# Extract the session data from the token.
|
||||||
|
nonce = self._get_value_from_macaroon(macaroon, "nonce")
|
||||||
|
idp_id = self._get_value_from_macaroon(macaroon, "idp_id")
|
||||||
|
client_redirect_url = self._get_value_from_macaroon(
|
||||||
|
macaroon, "client_redirect_url"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
ui_auth_session_id = self._get_value_from_macaroon(
|
||||||
|
macaroon, "ui_auth_session_id"
|
||||||
|
) # type: Optional[str]
|
||||||
|
except ValueError:
|
||||||
|
ui_auth_session_id = None
|
||||||
|
|
||||||
|
return OidcSessionData(
|
||||||
|
nonce=nonce,
|
||||||
|
idp_id=idp_id,
|
||||||
|
client_redirect_url=client_redirect_url,
|
||||||
|
ui_auth_session_id=ui_auth_session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_value_from_macaroon(self, macaroon: pymacaroons.Macaroon, key: str) -> str:
|
||||||
|
"""Extracts a caveat value from a macaroon token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
macaroon: the token
|
||||||
|
key: the key of the caveat to extract
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The extracted value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if the caveat was not in the macaroon
|
||||||
|
"""
|
||||||
|
prefix = key + " = "
|
||||||
|
for caveat in macaroon.caveats:
|
||||||
|
if caveat.caveat_id.startswith(prefix):
|
||||||
|
return caveat.caveat_id[len(prefix) :]
|
||||||
|
raise ValueError("No %s caveat in macaroon" % (key,))
|
||||||
|
|
||||||
|
def _verify_expiry(self, caveat: str) -> bool:
|
||||||
|
prefix = "time < "
|
||||||
|
if not caveat.startswith(prefix):
|
||||||
|
return False
|
||||||
|
expiry = int(caveat[len(prefix) :])
|
||||||
|
now = self._clock.time_msec()
|
||||||
|
return now < expiry
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(frozen=True, slots=True)
|
||||||
|
class OidcSessionData:
|
||||||
|
"""The attributes which are stored in a OIDC session cookie"""
|
||||||
|
|
||||||
|
# the Identity Provider being used
|
||||||
|
idp_id = attr.ib(type=str)
|
||||||
|
|
||||||
|
# The `nonce` parameter passed to the OIDC provider.
|
||||||
|
nonce = attr.ib(type=str)
|
||||||
|
|
||||||
|
# The URL the client gave when it initiated the flow. ("" if this is a UI Auth)
|
||||||
|
client_redirect_url = attr.ib(type=str)
|
||||||
|
|
||||||
|
# The session ID of the ongoing UI Auth (None if this is a login)
|
||||||
|
ui_auth_session_id = attr.ib(type=Optional[str], default=None)
|
||||||
|
|
||||||
|
|
||||||
UserAttributeDict = TypedDict(
|
UserAttributeDict = TypedDict(
|
||||||
"UserAttributeDict", {"localpart": Optional[str], "display_name": Optional[str]}
|
"UserAttributeDict", {"localpart": Optional[str], "display_name": Optional[str]}
|
||||||
)
|
)
|
||||||
|
@ -286,13 +286,19 @@ class ProfileHandler(BaseHandler):
|
|||||||
400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,)
|
400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
avatar_url_to_set = new_avatar_url # type: Optional[str]
|
||||||
|
if new_avatar_url == "":
|
||||||
|
avatar_url_to_set = None
|
||||||
|
|
||||||
# Same like set_displayname
|
# Same like set_displayname
|
||||||
if by_admin:
|
if by_admin:
|
||||||
requester = create_requester(
|
requester = create_requester(
|
||||||
target_user, authenticated_entity=requester.authenticated_entity
|
target_user, authenticated_entity=requester.authenticated_entity
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.store.set_profile_avatar_url(target_user.localpart, new_avatar_url)
|
await self.store.set_profile_avatar_url(
|
||||||
|
target_user.localpart, avatar_url_to_set
|
||||||
|
)
|
||||||
|
|
||||||
if self.hs.config.user_directory_search_all_users:
|
if self.hs.config.user_directory_search_all_users:
|
||||||
profile = await self.store.get_profileinfo(target_user.localpart)
|
profile = await self.store.get_profileinfo(target_user.localpart)
|
||||||
|
@ -31,8 +31,8 @@ class ReadMarkerHandler(BaseHandler):
|
|||||||
super().__init__(hs)
|
super().__init__(hs)
|
||||||
self.server_name = hs.config.server_name
|
self.server_name = hs.config.server_name
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
self.account_data_handler = hs.get_account_data_handler()
|
||||||
self.read_marker_linearizer = Linearizer(name="read_marker")
|
self.read_marker_linearizer = Linearizer(name="read_marker")
|
||||||
self.notifier = hs.get_notifier()
|
|
||||||
|
|
||||||
async def received_client_read_marker(
|
async def received_client_read_marker(
|
||||||
self, room_id: str, user_id: str, event_id: str
|
self, room_id: str, user_id: str, event_id: str
|
||||||
@ -59,7 +59,6 @@ class ReadMarkerHandler(BaseHandler):
|
|||||||
|
|
||||||
if should_update:
|
if should_update:
|
||||||
content = {"event_id": event_id}
|
content = {"event_id": event_id}
|
||||||
max_id = await self.store.add_account_data_to_room(
|
await self.account_data_handler.add_account_data_to_room(
|
||||||
user_id, room_id, "m.fully_read", content
|
user_id, room_id, "m.fully_read", content
|
||||||
)
|
)
|
||||||
self.notifier.on_new_event("account_data_key", max_id, users=[user_id])
|
|
||||||
|
@ -32,10 +32,26 @@ class ReceiptsHandler(BaseHandler):
|
|||||||
self.server_name = hs.config.server_name
|
self.server_name = hs.config.server_name
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.federation = hs.get_federation_sender()
|
|
||||||
hs.get_federation_registry().register_edu_handler(
|
# We only need to poke the federation sender explicitly if its on the
|
||||||
"m.receipt", self._received_remote_receipt
|
# same instance. Other federation sender instances will get notified by
|
||||||
)
|
# `synapse.app.generic_worker.FederationSenderHandler` when it sees it
|
||||||
|
# in the receipts stream.
|
||||||
|
self.federation_sender = None
|
||||||
|
if hs.should_send_federation():
|
||||||
|
self.federation_sender = hs.get_federation_sender()
|
||||||
|
|
||||||
|
# If we can handle the receipt EDUs we do so, otherwise we route them
|
||||||
|
# to the appropriate worker.
|
||||||
|
if hs.get_instance_name() in hs.config.worker.writers.receipts:
|
||||||
|
hs.get_federation_registry().register_edu_handler(
|
||||||
|
"m.receipt", self._received_remote_receipt
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
hs.get_federation_registry().register_instances_for_edu(
|
||||||
|
"m.receipt", hs.config.worker.writers.receipts,
|
||||||
|
)
|
||||||
|
|
||||||
self.clock = self.hs.get_clock()
|
self.clock = self.hs.get_clock()
|
||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
|
|
||||||
@ -125,7 +141,8 @@ class ReceiptsHandler(BaseHandler):
|
|||||||
if not is_new:
|
if not is_new:
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.federation.send_read_receipt(receipt)
|
if self.federation_sender:
|
||||||
|
await self.federation_sender.send_read_receipt(receipt)
|
||||||
|
|
||||||
|
|
||||||
class ReceiptEventSource:
|
class ReceiptEventSource:
|
||||||
|
@ -38,7 +38,6 @@ from synapse.api.filtering import Filter
|
|||||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
|
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.utils import copy_power_levels_contents
|
from synapse.events.utils import copy_power_levels_contents
|
||||||
from synapse.http.endpoint import parse_and_validate_server_name
|
|
||||||
from synapse.storage.state import StateFilter
|
from synapse.storage.state import StateFilter
|
||||||
from synapse.types import (
|
from synapse.types import (
|
||||||
JsonDict,
|
JsonDict,
|
||||||
@ -55,6 +54,7 @@ from synapse.types import (
|
|||||||
from synapse.util import stringutils
|
from synapse.util import stringutils
|
||||||
from synapse.util.async_helpers import Linearizer
|
from synapse.util.async_helpers import Linearizer
|
||||||
from synapse.util.caches.response_cache import ResponseCache
|
from synapse.util.caches.response_cache import ResponseCache
|
||||||
|
from synapse.util.stringutils import parse_and_validate_server_name
|
||||||
from synapse.visibility import filter_events_for_client
|
from synapse.visibility import filter_events_for_client
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
@ -63,6 +63,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||||||
self.registration_handler = hs.get_registration_handler()
|
self.registration_handler = hs.get_registration_handler()
|
||||||
self.profile_handler = hs.get_profile_handler()
|
self.profile_handler = hs.get_profile_handler()
|
||||||
self.event_creation_handler = hs.get_event_creation_handler()
|
self.event_creation_handler = hs.get_event_creation_handler()
|
||||||
|
self.account_data_handler = hs.get_account_data_handler()
|
||||||
|
|
||||||
self.member_linearizer = Linearizer(name="member")
|
self.member_linearizer = Linearizer(name="member")
|
||||||
|
|
||||||
@ -253,7 +254,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||||||
direct_rooms[key].append(new_room_id)
|
direct_rooms[key].append(new_room_id)
|
||||||
|
|
||||||
# Save back to user's m.direct account data
|
# Save back to user's m.direct account data
|
||||||
await self.store.add_account_data_for_user(
|
await self.account_data_handler.add_account_data_for_user(
|
||||||
user_id, AccountDataTypes.DIRECT, direct_rooms
|
user_id, AccountDataTypes.DIRECT, direct_rooms
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@ -263,7 +264,9 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
# Copy each room tag to the new room
|
# Copy each room tag to the new room
|
||||||
for tag, tag_content in room_tags.items():
|
for tag, tag_content in room_tags.items():
|
||||||
await self.store.add_tag_to_room(user_id, new_room_id, tag, tag_content)
|
await self.account_data_handler.add_tag_to_room(
|
||||||
|
user_id, new_room_id, tag, tag_content
|
||||||
|
)
|
||||||
|
|
||||||
async def update_membership(
|
async def update_membership(
|
||||||
self,
|
self,
|
||||||
|
@ -78,6 +78,10 @@ class SamlHandler(BaseHandler):
|
|||||||
# user-facing name of this auth provider
|
# user-facing name of this auth provider
|
||||||
self.idp_name = "SAML"
|
self.idp_name = "SAML"
|
||||||
|
|
||||||
|
# we do not currently support icons for SAML auth, but this is required by
|
||||||
|
# the SsoIdentityProvider protocol type.
|
||||||
|
self.idp_icon = None
|
||||||
|
|
||||||
# a map from saml session id to Saml2SessionData object
|
# a map from saml session id to Saml2SessionData object
|
||||||
self._outstanding_requests_dict = {} # type: Dict[str, Saml2SessionData]
|
self._outstanding_requests_dict = {} # type: Dict[str, Saml2SessionData]
|
||||||
|
|
||||||
|
@ -22,7 +22,10 @@ from typing_extensions import NoReturn, Protocol
|
|||||||
|
|
||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
|
|
||||||
|
from synapse.api.constants import LoginType
|
||||||
from synapse.api.errors import Codes, RedirectException, SynapseError
|
from synapse.api.errors import Codes, RedirectException, SynapseError
|
||||||
|
from synapse.handlers.ui_auth import UIAuthSessionDataConstants
|
||||||
|
from synapse.http import get_request_user_agent
|
||||||
from synapse.http.server import respond_with_html
|
from synapse.http.server import respond_with_html
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters
|
from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters
|
||||||
@ -72,6 +75,11 @@ class SsoIdentityProvider(Protocol):
|
|||||||
def idp_name(self) -> str:
|
def idp_name(self) -> str:
|
||||||
"""User-facing name for this provider"""
|
"""User-facing name for this provider"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def idp_icon(self) -> Optional[str]:
|
||||||
|
"""Optional MXC URI for user-facing icon"""
|
||||||
|
return None
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def handle_redirect_request(
|
async def handle_redirect_request(
|
||||||
self,
|
self,
|
||||||
@ -145,8 +153,13 @@ class SsoHandler:
|
|||||||
self._store = hs.get_datastore()
|
self._store = hs.get_datastore()
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
self._registration_handler = hs.get_registration_handler()
|
self._registration_handler = hs.get_registration_handler()
|
||||||
self._error_template = hs.config.sso_error_template
|
|
||||||
self._auth_handler = hs.get_auth_handler()
|
self._auth_handler = hs.get_auth_handler()
|
||||||
|
self._error_template = hs.config.sso_error_template
|
||||||
|
self._bad_user_template = hs.config.sso_auth_bad_user_template
|
||||||
|
|
||||||
|
# The following template is shown after a successful user interactive
|
||||||
|
# authentication session. It tells the user they can close the window.
|
||||||
|
self._sso_auth_success_template = hs.config.sso_auth_success_template
|
||||||
|
|
||||||
# a lock on the mappings
|
# a lock on the mappings
|
||||||
self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock())
|
self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock())
|
||||||
@ -166,6 +179,37 @@ class SsoHandler:
|
|||||||
"""Get the configured identity providers"""
|
"""Get the configured identity providers"""
|
||||||
return self._identity_providers
|
return self._identity_providers
|
||||||
|
|
||||||
|
async def get_identity_providers_for_user(
|
||||||
|
self, user_id: str
|
||||||
|
) -> Mapping[str, SsoIdentityProvider]:
|
||||||
|
"""Get the SsoIdentityProviders which a user has used
|
||||||
|
|
||||||
|
Given a user id, get the identity providers that that user has used to log in
|
||||||
|
with in the past (and thus could use to re-identify themselves for UI Auth).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: MXID of user to look up
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
a map of idp_id to SsoIdentityProvider
|
||||||
|
"""
|
||||||
|
external_ids = await self._store.get_external_ids_by_user(user_id)
|
||||||
|
|
||||||
|
valid_idps = {}
|
||||||
|
for idp_id, _ in external_ids:
|
||||||
|
idp = self._identity_providers.get(idp_id)
|
||||||
|
if not idp:
|
||||||
|
logger.warning(
|
||||||
|
"User %r has an SSO mapping for IdP %r, but this is no longer "
|
||||||
|
"configured.",
|
||||||
|
user_id,
|
||||||
|
idp_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
valid_idps[idp_id] = idp
|
||||||
|
|
||||||
|
return valid_idps
|
||||||
|
|
||||||
def render_error(
|
def render_error(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -362,7 +406,7 @@ class SsoHandler:
|
|||||||
attributes,
|
attributes,
|
||||||
auth_provider_id,
|
auth_provider_id,
|
||||||
remote_user_id,
|
remote_user_id,
|
||||||
request.get_user_agent(""),
|
get_request_user_agent(request),
|
||||||
request.getClientIP(),
|
request.getClientIP(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -545,20 +589,46 @@ class SsoHandler:
|
|||||||
auth_provider_id, remote_user_id,
|
auth_provider_id, remote_user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_id_to_verify = await self._auth_handler.get_session_data(
|
||||||
|
ui_auth_session_id, UIAuthSessionDataConstants.REQUEST_USER_ID
|
||||||
|
) # type: str
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Remote user %s/%s has not previously logged in here: UIA will fail",
|
"Remote user %s/%s has not previously logged in here: UIA will fail",
|
||||||
auth_provider_id,
|
auth_provider_id,
|
||||||
remote_user_id,
|
remote_user_id,
|
||||||
)
|
)
|
||||||
# Let the UIA flow handle this the same as if they presented creds for a
|
elif user_id != user_id_to_verify:
|
||||||
# different user.
|
logger.warning(
|
||||||
user_id = ""
|
"Remote user %s/%s mapped onto incorrect user %s: UIA will fail",
|
||||||
|
auth_provider_id,
|
||||||
|
remote_user_id,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# success!
|
||||||
|
# Mark the stage of the authentication as successful.
|
||||||
|
await self._store.mark_ui_auth_stage_complete(
|
||||||
|
ui_auth_session_id, LoginType.SSO, user_id
|
||||||
|
)
|
||||||
|
|
||||||
await self._auth_handler.complete_sso_ui_auth(
|
# Render the HTML confirmation page and return.
|
||||||
user_id, ui_auth_session_id, request
|
html = self._sso_auth_success_template
|
||||||
|
respond_with_html(request, 200, html)
|
||||||
|
return
|
||||||
|
|
||||||
|
# the user_id didn't match: mark the stage of the authentication as unsuccessful
|
||||||
|
await self._store.mark_ui_auth_stage_complete(
|
||||||
|
ui_auth_session_id, LoginType.SSO, ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# render an error page.
|
||||||
|
html = self._bad_user_template.render(
|
||||||
|
server_name=self._server_name, user_id_to_verify=user_id_to_verify,
|
||||||
|
)
|
||||||
|
respond_with_html(request, 200, html)
|
||||||
|
|
||||||
async def check_username_availability(
|
async def check_username_availability(
|
||||||
self, localpart: str, session_id: str,
|
self, localpart: str, session_id: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -628,7 +698,7 @@ class SsoHandler:
|
|||||||
attributes,
|
attributes,
|
||||||
session.auth_provider_id,
|
session.auth_provider_id,
|
||||||
session.remote_user_id,
|
session.remote_user_id,
|
||||||
request.get_user_agent(""),
|
get_request_user_agent(request),
|
||||||
request.getClientIP(),
|
request.getClientIP(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,3 +20,18 @@ TODO: move more stuff out of AuthHandler in here.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from synapse.handlers.ui_auth.checkers import INTERACTIVE_AUTH_CHECKERS # noqa: F401
|
from synapse.handlers.ui_auth.checkers import INTERACTIVE_AUTH_CHECKERS # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
class UIAuthSessionDataConstants:
|
||||||
|
"""Constants for use with AuthHandler.set_session_data"""
|
||||||
|
|
||||||
|
# used during registration and password reset to store a hashed copy of the
|
||||||
|
# password, so that the client does not need to submit it each time.
|
||||||
|
PASSWORD_HASH = "password_hash"
|
||||||
|
|
||||||
|
# used during registration to store the mxid of the registered user
|
||||||
|
REGISTERED_USER_ID = "registered_user_id"
|
||||||
|
|
||||||
|
# used by validate_user_via_ui_auth to store the mxid of the user we are validating
|
||||||
|
# for.
|
||||||
|
REQUEST_USER_ID = "request_user_id"
|
||||||
|
@ -17,6 +17,7 @@ import re
|
|||||||
|
|
||||||
from twisted.internet import task
|
from twisted.internet import task
|
||||||
from twisted.web.client import FileBodyProducer
|
from twisted.web.client import FileBodyProducer
|
||||||
|
from twisted.web.iweb import IRequest
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
|
||||||
@ -50,3 +51,17 @@ class QuieterFileBodyProducer(FileBodyProducer):
|
|||||||
FileBodyProducer.stopProducing(self)
|
FileBodyProducer.stopProducing(self)
|
||||||
except task.TaskStopped:
|
except task.TaskStopped:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_user_agent(request: IRequest, default: str = "") -> str:
|
||||||
|
"""Return the last User-Agent header, or the given default.
|
||||||
|
"""
|
||||||
|
# There could be raw utf-8 bytes in the User-Agent header.
|
||||||
|
|
||||||
|
# N.B. if you don't do this, the logger explodes cryptically
|
||||||
|
# with maximum recursion trying to log errors about
|
||||||
|
# the charset problem.
|
||||||
|
# c.f. https://github.com/matrix-org/synapse/issues/3471
|
||||||
|
|
||||||
|
h = request.getHeader(b"User-Agent")
|
||||||
|
return h.decode("ascii", "replace") if h else default
|
||||||
|
@ -32,7 +32,7 @@ from typing import (
|
|||||||
|
|
||||||
import treq
|
import treq
|
||||||
from canonicaljson import encode_canonical_json
|
from canonicaljson import encode_canonical_json
|
||||||
from netaddr import IPAddress, IPSet
|
from netaddr import AddrFormatError, IPAddress, IPSet
|
||||||
from prometheus_client import Counter
|
from prometheus_client import Counter
|
||||||
from zope.interface import implementer, provider
|
from zope.interface import implementer, provider
|
||||||
|
|
||||||
@ -261,16 +261,16 @@ class BlacklistingAgentWrapper(Agent):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ip_address = IPAddress(h.hostname)
|
ip_address = IPAddress(h.hostname)
|
||||||
|
except AddrFormatError:
|
||||||
|
# Not an IP
|
||||||
|
pass
|
||||||
|
else:
|
||||||
if check_against_blacklist(
|
if check_against_blacklist(
|
||||||
ip_address, self._ip_whitelist, self._ip_blacklist
|
ip_address, self._ip_whitelist, self._ip_blacklist
|
||||||
):
|
):
|
||||||
logger.info("Blocking access to %s due to blacklist" % (ip_address,))
|
logger.info("Blocking access to %s due to blacklist" % (ip_address,))
|
||||||
e = SynapseError(403, "IP address blocked by IP blacklist entry")
|
e = SynapseError(403, "IP address blocked by IP blacklist entry")
|
||||||
return defer.fail(Failure(e))
|
return defer.fail(Failure(e))
|
||||||
except Exception:
|
|
||||||
# Not an IP
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self._agent.request(
|
return self._agent.request(
|
||||||
method, uri, headers=headers, bodyProducer=bodyProducer
|
method, uri, headers=headers, bodyProducer=bodyProducer
|
||||||
@ -341,6 +341,7 @@ class SimpleHttpClient:
|
|||||||
|
|
||||||
self.agent = ProxyAgent(
|
self.agent = ProxyAgent(
|
||||||
self.reactor,
|
self.reactor,
|
||||||
|
hs.get_reactor(),
|
||||||
connectTimeout=15,
|
connectTimeout=15,
|
||||||
contextFactory=self.hs.get_http_client_context_factory(),
|
contextFactory=self.hs.get_http_client_context_factory(),
|
||||||
pool=pool,
|
pool=pool,
|
||||||
@ -723,7 +724,7 @@ class SimpleHttpClient:
|
|||||||
read_body_with_max_size(response, output_stream, max_size)
|
read_body_with_max_size(response, output_stream, max_size)
|
||||||
)
|
)
|
||||||
except BodyExceededMaxSize:
|
except BodyExceededMaxSize:
|
||||||
SynapseError(
|
raise SynapseError(
|
||||||
502,
|
502,
|
||||||
"Requested file is too large > %r bytes" % (max_size,),
|
"Requested file is too large > %r bytes" % (max_size,),
|
||||||
Codes.TOO_LARGE,
|
Codes.TOO_LARGE,
|
||||||
@ -765,14 +766,24 @@ class _ReadBodyWithMaxSizeProtocol(protocol.Protocol):
|
|||||||
self.max_size = max_size
|
self.max_size = max_size
|
||||||
|
|
||||||
def dataReceived(self, data: bytes) -> None:
|
def dataReceived(self, data: bytes) -> None:
|
||||||
|
# If the deferred was called, bail early.
|
||||||
|
if self.deferred.called:
|
||||||
|
return
|
||||||
|
|
||||||
self.stream.write(data)
|
self.stream.write(data)
|
||||||
self.length += len(data)
|
self.length += len(data)
|
||||||
|
# The first time the maximum size is exceeded, error and cancel the
|
||||||
|
# connection. dataReceived might be called again if data was received
|
||||||
|
# in the meantime.
|
||||||
if self.max_size is not None and self.length >= self.max_size:
|
if self.max_size is not None and self.length >= self.max_size:
|
||||||
self.deferred.errback(BodyExceededMaxSize())
|
self.deferred.errback(BodyExceededMaxSize())
|
||||||
self.deferred = defer.Deferred()
|
|
||||||
self.transport.loseConnection()
|
self.transport.loseConnection()
|
||||||
|
|
||||||
def connectionLost(self, reason: Failure) -> None:
|
def connectionLost(self, reason: Failure) -> None:
|
||||||
|
# If the maximum size was already exceeded, there's nothing to do.
|
||||||
|
if self.deferred.called:
|
||||||
|
return
|
||||||
|
|
||||||
if reason.check(ResponseDone):
|
if reason.check(ResponseDone):
|
||||||
self.deferred.callback(self.length)
|
self.deferred.callback(self.length)
|
||||||
elif reason.check(PotentialDataLoss):
|
elif reason.check(PotentialDataLoss):
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014-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 re
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_server_name(server_name):
|
|
||||||
"""Split a server name into host/port parts.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
server_name (str): server name to parse
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[str, int|None]: host/port parts.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError if the server name could not be parsed.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if server_name[-1] == "]":
|
|
||||||
# ipv6 literal, hopefully
|
|
||||||
return server_name, None
|
|
||||||
|
|
||||||
domain_port = server_name.rsplit(":", 1)
|
|
||||||
domain = domain_port[0]
|
|
||||||
port = int(domain_port[1]) if domain_port[1:] else None
|
|
||||||
return domain, port
|
|
||||||
except Exception:
|
|
||||||
raise ValueError("Invalid server name '%s'" % server_name)
|
|
||||||
|
|
||||||
|
|
||||||
VALID_HOST_REGEX = re.compile("\\A[0-9a-zA-Z.-]+\\Z")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_and_validate_server_name(server_name):
|
|
||||||
"""Split a server name into host/port parts and do some basic validation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
server_name (str): server name to parse
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[str, int|None]: host/port parts.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError if the server name could not be parsed.
|
|
||||||
"""
|
|
||||||
host, port = parse_server_name(server_name)
|
|
||||||
|
|
||||||
# these tests don't need to be bulletproof as we'll find out soon enough
|
|
||||||
# if somebody is giving us invalid data. What we *do* need is to be sure
|
|
||||||
# that nobody is sneaking IP literals in that look like hostnames, etc.
|
|
||||||
|
|
||||||
# look for ipv6 literals
|
|
||||||
if host[0] == "[":
|
|
||||||
if host[-1] != "]":
|
|
||||||
raise ValueError("Mismatched [...] in server name '%s'" % (server_name,))
|
|
||||||
return host, port
|
|
||||||
|
|
||||||
# otherwise it should only be alphanumerics.
|
|
||||||
if not VALID_HOST_REGEX.match(host):
|
|
||||||
raise ValueError(
|
|
||||||
"Server name '%s' contains invalid characters" % (server_name,)
|
|
||||||
)
|
|
||||||
|
|
||||||
return host, port
|
|
@ -102,7 +102,6 @@ class MatrixFederationAgent:
|
|||||||
pool=self._pool,
|
pool=self._pool,
|
||||||
contextFactory=tls_client_options_factory,
|
contextFactory=tls_client_options_factory,
|
||||||
),
|
),
|
||||||
self._reactor,
|
|
||||||
ip_blacklist=ip_blacklist,
|
ip_blacklist=ip_blacklist,
|
||||||
),
|
),
|
||||||
user_agent=self.user_agent,
|
user_agent=self.user_agent,
|
||||||
|
@ -174,6 +174,16 @@ async def _handle_json_response(
|
|||||||
d = timeout_deferred(d, timeout=timeout_sec, reactor=reactor)
|
d = timeout_deferred(d, timeout=timeout_sec, reactor=reactor)
|
||||||
|
|
||||||
body = await make_deferred_yieldable(d)
|
body = await make_deferred_yieldable(d)
|
||||||
|
except ValueError as e:
|
||||||
|
# The JSON content was invalid.
|
||||||
|
logger.warning(
|
||||||
|
"{%s} [%s] Failed to parse JSON response - %s %s",
|
||||||
|
request.txn_id,
|
||||||
|
request.destination,
|
||||||
|
request.method,
|
||||||
|
request.uri.decode("ascii"),
|
||||||
|
)
|
||||||
|
raise RequestSendFailed(e, can_retry=False) from e
|
||||||
except defer.TimeoutError as e:
|
except defer.TimeoutError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"{%s} [%s] Timed out reading response - %s %s",
|
"{%s} [%s] Timed out reading response - %s %s",
|
||||||
@ -986,7 +996,7 @@ class MatrixFederationHttpClient:
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
"{%s} [%s] %s", request.txn_id, request.destination, msg,
|
"{%s} [%s] %s", request.txn_id, request.destination, msg,
|
||||||
)
|
)
|
||||||
SynapseError(502, msg, Codes.TOO_LARGE)
|
raise SynapseError(502, msg, Codes.TOO_LARGE)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"{%s} [%s] Error reading response: %s",
|
"{%s} [%s] Error reading response: %s",
|
||||||
|
@ -39,6 +39,10 @@ class ProxyAgent(_AgentBase):
|
|||||||
reactor: twisted reactor to place outgoing
|
reactor: twisted reactor to place outgoing
|
||||||
connections.
|
connections.
|
||||||
|
|
||||||
|
proxy_reactor: twisted reactor to use for connections to the proxy server
|
||||||
|
reactor might have some blacklisting applied (i.e. for DNS queries),
|
||||||
|
but we need unblocked access to the proxy.
|
||||||
|
|
||||||
contextFactory (IPolicyForHTTPS): A factory for TLS contexts, to control the
|
contextFactory (IPolicyForHTTPS): A factory for TLS contexts, to control the
|
||||||
verification parameters of OpenSSL. The default is to use a
|
verification parameters of OpenSSL. The default is to use a
|
||||||
`BrowserLikePolicyForHTTPS`, so unless you have special
|
`BrowserLikePolicyForHTTPS`, so unless you have special
|
||||||
@ -59,6 +63,7 @@ class ProxyAgent(_AgentBase):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
reactor,
|
reactor,
|
||||||
|
proxy_reactor=None,
|
||||||
contextFactory=BrowserLikePolicyForHTTPS(),
|
contextFactory=BrowserLikePolicyForHTTPS(),
|
||||||
connectTimeout=None,
|
connectTimeout=None,
|
||||||
bindAddress=None,
|
bindAddress=None,
|
||||||
@ -68,6 +73,11 @@ class ProxyAgent(_AgentBase):
|
|||||||
):
|
):
|
||||||
_AgentBase.__init__(self, reactor, pool)
|
_AgentBase.__init__(self, reactor, pool)
|
||||||
|
|
||||||
|
if proxy_reactor is None:
|
||||||
|
self.proxy_reactor = reactor
|
||||||
|
else:
|
||||||
|
self.proxy_reactor = proxy_reactor
|
||||||
|
|
||||||
self._endpoint_kwargs = {}
|
self._endpoint_kwargs = {}
|
||||||
if connectTimeout is not None:
|
if connectTimeout is not None:
|
||||||
self._endpoint_kwargs["timeout"] = connectTimeout
|
self._endpoint_kwargs["timeout"] = connectTimeout
|
||||||
@ -75,11 +85,11 @@ class ProxyAgent(_AgentBase):
|
|||||||
self._endpoint_kwargs["bindAddress"] = bindAddress
|
self._endpoint_kwargs["bindAddress"] = bindAddress
|
||||||
|
|
||||||
self.http_proxy_endpoint = _http_proxy_endpoint(
|
self.http_proxy_endpoint = _http_proxy_endpoint(
|
||||||
http_proxy, reactor, **self._endpoint_kwargs
|
http_proxy, self.proxy_reactor, **self._endpoint_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
self.https_proxy_endpoint = _http_proxy_endpoint(
|
self.https_proxy_endpoint = _http_proxy_endpoint(
|
||||||
https_proxy, reactor, **self._endpoint_kwargs
|
https_proxy, self.proxy_reactor, **self._endpoint_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
self._policy_for_https = contextFactory
|
self._policy_for_https = contextFactory
|
||||||
@ -137,7 +147,7 @@ class ProxyAgent(_AgentBase):
|
|||||||
request_path = uri
|
request_path = uri
|
||||||
elif parsed_uri.scheme == b"https" and self.https_proxy_endpoint:
|
elif parsed_uri.scheme == b"https" and self.https_proxy_endpoint:
|
||||||
endpoint = HTTPConnectProxyEndpoint(
|
endpoint = HTTPConnectProxyEndpoint(
|
||||||
self._reactor,
|
self.proxy_reactor,
|
||||||
self.https_proxy_endpoint,
|
self.https_proxy_endpoint,
|
||||||
parsed_uri.host,
|
parsed_uri.host,
|
||||||
parsed_uri.port,
|
parsed_uri.port,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user