diff options
Diffstat (limited to 'tests/rest/client/v1')
-rw-r--r-- | tests/rest/client/v1/__init__.py | 13 | ||||
-rw-r--r-- | tests/rest/client/v1/test_directory.py | 154 | ||||
-rw-r--r-- | tests/rest/client/v1/test_events.py | 156 | ||||
-rw-r--r-- | tests/rest/client/v1/test_login.py | 1345 | ||||
-rw-r--r-- | tests/rest/client/v1/test_presence.py | 76 | ||||
-rw-r--r-- | tests/rest/client/v1/test_profile.py | 270 | ||||
-rw-r--r-- | tests/rest/client/v1/test_push_rule_attrs.py | 414 | ||||
-rw-r--r-- | tests/rest/client/v1/test_rooms.py | 2150 | ||||
-rw-r--r-- | tests/rest/client/v1/test_typing.py | 121 | ||||
-rw-r--r-- | tests/rest/client/v1/utils.py | 654 |
10 files changed, 0 insertions, 5353 deletions
diff --git a/tests/rest/client/v1/__init__.py b/tests/rest/client/v1/__init__.py deleted file mode 100644 index 5e83dba2ed..0000000000 --- a/tests/rest/client/v1/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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. diff --git a/tests/rest/client/v1/test_directory.py b/tests/rest/client/v1/test_directory.py deleted file mode 100644 index d2181ea907..0000000000 --- a/tests/rest/client/v1/test_directory.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2019 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json - -from synapse.rest import admin -from synapse.rest.client import directory, login, room -from synapse.types import RoomAlias -from synapse.util.stringutils import random_string - -from tests import unittest -from tests.unittest import override_config - - -class DirectoryTestCase(unittest.HomeserverTestCase): - - servlets = [ - admin.register_servlets_for_client_rest_resource, - directory.register_servlets, - login.register_servlets, - room.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - config = self.default_config() - config["require_membership_for_aliases"] = True - - self.hs = self.setup_test_homeserver(config=config) - - return self.hs - - def prepare(self, reactor, clock, homeserver): - self.room_owner = self.register_user("room_owner", "test") - self.room_owner_tok = self.login("room_owner", "test") - - self.room_id = self.helper.create_room_as( - self.room_owner, tok=self.room_owner_tok - ) - - self.user = self.register_user("user", "test") - self.user_tok = self.login("user", "test") - - def test_state_event_not_in_room(self): - self.ensure_user_left_room() - self.set_alias_via_state_event(403) - - def test_directory_endpoint_not_in_room(self): - self.ensure_user_left_room() - self.set_alias_via_directory(403) - - def test_state_event_in_room_too_long(self): - self.ensure_user_joined_room() - self.set_alias_via_state_event(400, alias_length=256) - - def test_directory_in_room_too_long(self): - self.ensure_user_joined_room() - self.set_alias_via_directory(400, alias_length=256) - - @override_config({"default_room_version": 5}) - def test_state_event_user_in_v5_room(self): - """Test that a regular user can add alias events before room v6""" - self.ensure_user_joined_room() - self.set_alias_via_state_event(200) - - @override_config({"default_room_version": 6}) - def test_state_event_v6_room(self): - """Test that a regular user can *not* add alias events from room v6""" - self.ensure_user_joined_room() - self.set_alias_via_state_event(403) - - def test_directory_in_room(self): - self.ensure_user_joined_room() - self.set_alias_via_directory(200) - - def test_room_creation_too_long(self): - url = "/_matrix/client/r0/createRoom" - - # We use deliberately a localpart under the length threshold so - # that we can make sure that the check is done on the whole alias. - data = {"room_alias_name": random_string(256 - len(self.hs.hostname))} - request_data = json.dumps(data) - channel = self.make_request( - "POST", url, request_data, access_token=self.user_tok - ) - self.assertEqual(channel.code, 400, channel.result) - - def test_room_creation(self): - url = "/_matrix/client/r0/createRoom" - - # Check with an alias of allowed length. There should already be - # a test that ensures it works in test_register.py, but let's be - # as cautious as possible here. - data = {"room_alias_name": random_string(5)} - request_data = json.dumps(data) - channel = self.make_request( - "POST", url, request_data, access_token=self.user_tok - ) - self.assertEqual(channel.code, 200, channel.result) - - def set_alias_via_state_event(self, expected_code, alias_length=5): - url = "/_matrix/client/r0/rooms/%s/state/m.room.aliases/%s" % ( - self.room_id, - self.hs.hostname, - ) - - data = {"aliases": [self.random_alias(alias_length)]} - request_data = json.dumps(data) - - channel = self.make_request( - "PUT", url, request_data, access_token=self.user_tok - ) - self.assertEqual(channel.code, expected_code, channel.result) - - def set_alias_via_directory(self, expected_code, alias_length=5): - url = "/_matrix/client/r0/directory/room/%s" % self.random_alias(alias_length) - data = {"room_id": self.room_id} - request_data = json.dumps(data) - - channel = self.make_request( - "PUT", url, request_data, access_token=self.user_tok - ) - self.assertEqual(channel.code, expected_code, channel.result) - - def random_alias(self, length): - return RoomAlias(random_string(length), self.hs.hostname).to_string() - - def ensure_user_left_room(self): - self.ensure_membership("leave") - - def ensure_user_joined_room(self): - self.ensure_membership("join") - - def ensure_membership(self, membership): - try: - if membership == "leave": - self.helper.leave(room=self.room_id, user=self.user, tok=self.user_tok) - if membership == "join": - self.helper.join(room=self.room_id, user=self.user, tok=self.user_tok) - except AssertionError: - # We don't care whether the leave request didn't return a 200 (e.g. - # if the user isn't already in the room), because we only want to - # make sure the user isn't in the room. - pass diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py deleted file mode 100644 index a90294003e..0000000000 --- a/tests/rest/client/v1/test_events.py +++ /dev/null @@ -1,156 +0,0 @@ -# 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. - -""" Tests REST events for /events paths.""" - -from unittest.mock import Mock - -import synapse.rest.admin -from synapse.rest.client import events, login, room - -from tests import unittest - - -class EventStreamPermissionsTestCase(unittest.HomeserverTestCase): - """Tests event streaming (GET /events).""" - - servlets = [ - events.register_servlets, - room.register_servlets, - synapse.rest.admin.register_servlets_for_client_rest_resource, - login.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - - config = self.default_config() - config["enable_registration_captcha"] = False - config["enable_registration"] = True - config["auto_join_rooms"] = [] - - hs = self.setup_test_homeserver(config=config) - - hs.get_federation_handler = Mock() - - return hs - - def prepare(self, reactor, clock, hs): - - # register an account - self.user_id = self.register_user("sid1", "pass") - self.token = self.login(self.user_id, "pass") - - # register a 2nd account - self.other_user = self.register_user("other2", "pass") - self.other_token = self.login(self.other_user, "pass") - - def test_stream_basic_permissions(self): - # invalid token, expect 401 - # note: this is in violation of the original v1 spec, which expected - # 403. However, since the v1 spec no longer exists and the v1 - # implementation is now part of the r0 implementation, the newer - # behaviour is used instead to be consistent with the r0 spec. - # see issue #2602 - channel = self.make_request( - "GET", "/events?access_token=%s" % ("invalid" + self.token,) - ) - self.assertEquals(channel.code, 401, msg=channel.result) - - # valid token, expect content - channel = self.make_request( - "GET", "/events?access_token=%s&timeout=0" % (self.token,) - ) - self.assertEquals(channel.code, 200, msg=channel.result) - self.assertTrue("chunk" in channel.json_body) - self.assertTrue("start" in channel.json_body) - self.assertTrue("end" in channel.json_body) - - def test_stream_room_permissions(self): - room_id = self.helper.create_room_as(self.other_user, tok=self.other_token) - self.helper.send(room_id, tok=self.other_token) - - # invited to room (expect no content for room) - self.helper.invite( - room_id, src=self.other_user, targ=self.user_id, tok=self.other_token - ) - - # valid token, expect content - channel = self.make_request( - "GET", "/events?access_token=%s&timeout=0" % (self.token,) - ) - self.assertEquals(channel.code, 200, msg=channel.result) - - # We may get a presence event for ourselves down - self.assertEquals( - 0, - len( - [ - c - for c in channel.json_body["chunk"] - if not ( - c.get("type") == "m.presence" - and c["content"].get("user_id") == self.user_id - ) - ] - ), - ) - - # joined room (expect all content for room) - self.helper.join(room=room_id, user=self.user_id, tok=self.token) - - # left to room (expect no content for room) - - def TODO_test_stream_items(self): - # new user, no content - - # join room, expect 1 item (join) - - # send message, expect 2 items (join,send) - - # set topic, expect 3 items (join,send,topic) - - # someone else join room, expect 4 (join,send,topic,join) - - # someone else send message, expect 5 (join,send.topic,join,send) - - # someone else set topic, expect 6 (join,send,topic,join,send,topic) - pass - - -class GetEventsTestCase(unittest.HomeserverTestCase): - servlets = [ - events.register_servlets, - room.register_servlets, - synapse.rest.admin.register_servlets_for_client_rest_resource, - login.register_servlets, - ] - - def prepare(self, hs, reactor, clock): - - # register an account - self.user_id = self.register_user("sid1", "pass") - self.token = self.login(self.user_id, "pass") - - self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) - - def test_get_event_via_events(self): - resp = self.helper.send(self.room_id, tok=self.token) - event_id = resp["event_id"] - - channel = self.make_request( - "GET", - "/events/" + event_id, - access_token=self.token, - ) - self.assertEquals(channel.code, 200, msg=channel.result) diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py deleted file mode 100644 index eba3552b19..0000000000 --- a/tests/rest/client/v1/test_login.py +++ /dev/null @@ -1,1345 +0,0 @@ -# Copyright 2019-2021 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -import urllib.parse -from typing import Any, Dict, List, Optional, Union -from unittest.mock import Mock -from urllib.parse import urlencode - -import pymacaroons - -from twisted.web.resource import Resource - -import synapse.rest.admin -from synapse.appservice import ApplicationService -from synapse.rest.client import devices, login, logout, register -from synapse.rest.client.account import WhoamiRestServlet -from synapse.rest.synapse.client import build_synapse_client_resource_tree -from synapse.types import create_requester - -from tests import unittest -from tests.handlers.test_oidc import HAS_OIDC -from tests.handlers.test_saml import has_saml2 -from tests.rest.client.v1.utils import TEST_OIDC_AUTH_ENDPOINT, TEST_OIDC_CONFIG -from tests.test_utils.html_parsers import TestHtmlParser -from tests.unittest import HomeserverTestCase, override_config, skip_unless - -try: - import jwt - - HAS_JWT = True -except ImportError: - HAS_JWT = False - - -# synapse server name: used to populate public_baseurl in some tests -SYNAPSE_SERVER_PUBLIC_HOSTNAME = "synapse" - -# public_baseurl for some tests. It uses an http:// scheme because -# FakeChannel.isSecure() returns False, so synapse will see the requested uri as -# http://..., so using http in the public_baseurl stops Synapse trying to redirect to -# https://.... -BASE_URL = "http://%s/" % (SYNAPSE_SERVER_PUBLIC_HOSTNAME,) - -# CAS server used in some tests -CAS_SERVER = "https://fake.test" - -# just enough to tell pysaml2 where to redirect to -SAML_SERVER = "https://test.saml.server/idp/sso" -TEST_SAML_METADATA = """ -<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"> - <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> - <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="%(SAML_SERVER)s"/> - </md:IDPSSODescriptor> -</md:EntityDescriptor> -""" % { - "SAML_SERVER": SAML_SERVER, -} - -LOGIN_URL = b"/_matrix/client/r0/login" -TEST_URL = b"/_matrix/client/r0/account/whoami" - -# a (valid) url with some annoying characters in. %3D is =, %26 is &, %2B is + -TEST_CLIENT_REDIRECT_URL = 'https://x?<ab c>&q"+%3D%2B"="fö%26=o"' - -# the query params in TEST_CLIENT_REDIRECT_URL -EXPECTED_CLIENT_REDIRECT_URL_PARAMS = [("<ab c>", ""), ('q" =+"', '"fö&=o"')] - -# (possibly experimental) login flows we expect to appear in the list after the normal -# ones -ADDITIONAL_LOGIN_FLOWS = [{"type": "uk.half-shot.msc2778.login.application_service"}] - - -class LoginRestServletTestCase(unittest.HomeserverTestCase): - - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - login.register_servlets, - logout.register_servlets, - devices.register_servlets, - lambda hs, http_server: WhoamiRestServlet(hs).register(http_server), - ] - - def make_homeserver(self, reactor, clock): - self.hs = self.setup_test_homeserver() - self.hs.config.enable_registration = True - self.hs.config.registrations_require_3pid = [] - self.hs.config.auto_join_rooms = [] - self.hs.config.enable_registration_captcha = False - - return self.hs - - @override_config( - { - "rc_login": { - "address": {"per_second": 0.17, "burst_count": 5}, - # Prevent the account login ratelimiter from raising first - # - # This is normally covered by the default test homeserver config - # which sets these values to 10000, but as we're overriding the entire - # rc_login dict here, we need to set this manually as well - "account": {"per_second": 10000, "burst_count": 10000}, - } - } - ) - def test_POST_ratelimiting_per_address(self): - # Create different users so we're sure not to be bothered by the per-user - # ratelimiter. - for i in range(0, 6): - self.register_user("kermit" + str(i), "monkey") - - for i in range(0, 6): - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit" + str(i)}, - "password": "monkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - if i == 5: - self.assertEquals(channel.result["code"], b"429", channel.result) - retry_after_ms = int(channel.json_body["retry_after_ms"]) - else: - self.assertEquals(channel.result["code"], b"200", channel.result) - - # Since we're ratelimiting at 1 request/min, retry_after_ms should be lower - # than 1min. - self.assertTrue(retry_after_ms < 6000) - - self.reactor.advance(retry_after_ms / 1000.0 + 1.0) - - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit" + str(i)}, - "password": "monkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - self.assertEquals(channel.result["code"], b"200", channel.result) - - @override_config( - { - "rc_login": { - "account": {"per_second": 0.17, "burst_count": 5}, - # Prevent the address login ratelimiter from raising first - # - # This is normally covered by the default test homeserver config - # which sets these values to 10000, but as we're overriding the entire - # rc_login dict here, we need to set this manually as well - "address": {"per_second": 10000, "burst_count": 10000}, - } - } - ) - def test_POST_ratelimiting_per_account(self): - self.register_user("kermit", "monkey") - - for i in range(0, 6): - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit"}, - "password": "monkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - if i == 5: - self.assertEquals(channel.result["code"], b"429", channel.result) - retry_after_ms = int(channel.json_body["retry_after_ms"]) - else: - self.assertEquals(channel.result["code"], b"200", channel.result) - - # Since we're ratelimiting at 1 request/min, retry_after_ms should be lower - # than 1min. - self.assertTrue(retry_after_ms < 6000) - - self.reactor.advance(retry_after_ms / 1000.0) - - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit"}, - "password": "monkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - self.assertEquals(channel.result["code"], b"200", channel.result) - - @override_config( - { - "rc_login": { - # Prevent the address login ratelimiter from raising first - # - # This is normally covered by the default test homeserver config - # which sets these values to 10000, but as we're overriding the entire - # rc_login dict here, we need to set this manually as well - "address": {"per_second": 10000, "burst_count": 10000}, - "failed_attempts": {"per_second": 0.17, "burst_count": 5}, - } - } - ) - def test_POST_ratelimiting_per_account_failed_attempts(self): - self.register_user("kermit", "monkey") - - for i in range(0, 6): - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit"}, - "password": "notamonkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - if i == 5: - self.assertEquals(channel.result["code"], b"429", channel.result) - retry_after_ms = int(channel.json_body["retry_after_ms"]) - else: - self.assertEquals(channel.result["code"], b"403", channel.result) - - # Since we're ratelimiting at 1 request/min, retry_after_ms should be lower - # than 1min. - self.assertTrue(retry_after_ms < 6000) - - self.reactor.advance(retry_after_ms / 1000.0 + 1.0) - - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit"}, - "password": "notamonkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - self.assertEquals(channel.result["code"], b"403", channel.result) - - @override_config({"session_lifetime": "24h"}) - def test_soft_logout(self): - self.register_user("kermit", "monkey") - - # we shouldn't be able to make requests without an access token - channel = self.make_request(b"GET", TEST_URL) - self.assertEquals(channel.result["code"], b"401", channel.result) - self.assertEquals(channel.json_body["errcode"], "M_MISSING_TOKEN") - - # log in as normal - params = { - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": "kermit"}, - "password": "monkey", - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - self.assertEquals(channel.code, 200, channel.result) - access_token = channel.json_body["access_token"] - device_id = channel.json_body["device_id"] - - # we should now be able to make requests with the access token - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 200, channel.result) - - # time passes - self.reactor.advance(24 * 3600) - - # ... and we should be soft-logouted - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 401, channel.result) - self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN") - self.assertEquals(channel.json_body["soft_logout"], True) - - # - # test behaviour after deleting the expired device - # - - # we now log in as a different device - access_token_2 = self.login("kermit", "monkey") - - # more requests with the expired token should still return a soft-logout - self.reactor.advance(3600) - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 401, channel.result) - self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN") - self.assertEquals(channel.json_body["soft_logout"], True) - - # ... but if we delete that device, it will be a proper logout - self._delete_device(access_token_2, "kermit", "monkey", device_id) - - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 401, channel.result) - self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN") - self.assertEquals(channel.json_body["soft_logout"], False) - - def _delete_device(self, access_token, user_id, password, device_id): - """Perform the UI-Auth to delete a device""" - channel = self.make_request( - b"DELETE", "devices/" + device_id, access_token=access_token - ) - self.assertEquals(channel.code, 401, channel.result) - # check it's a UI-Auth fail - self.assertEqual( - set(channel.json_body.keys()), - {"flows", "params", "session"}, - channel.result, - ) - - auth = { - "type": "m.login.password", - # https://github.com/matrix-org/synapse/issues/5665 - # "identifier": {"type": "m.id.user", "user": user_id}, - "user": user_id, - "password": password, - "session": channel.json_body["session"], - } - - channel = self.make_request( - b"DELETE", - "devices/" + device_id, - access_token=access_token, - content={"auth": auth}, - ) - self.assertEquals(channel.code, 200, channel.result) - - @override_config({"session_lifetime": "24h"}) - def test_session_can_hard_logout_after_being_soft_logged_out(self): - self.register_user("kermit", "monkey") - - # log in as normal - access_token = self.login("kermit", "monkey") - - # we should now be able to make requests with the access token - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 200, channel.result) - - # time passes - self.reactor.advance(24 * 3600) - - # ... and we should be soft-logouted - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 401, channel.result) - self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN") - self.assertEquals(channel.json_body["soft_logout"], True) - - # Now try to hard logout this session - channel = self.make_request(b"POST", "/logout", access_token=access_token) - self.assertEquals(channel.result["code"], b"200", channel.result) - - @override_config({"session_lifetime": "24h"}) - def test_session_can_hard_logout_all_sessions_after_being_soft_logged_out(self): - self.register_user("kermit", "monkey") - - # log in as normal - access_token = self.login("kermit", "monkey") - - # we should now be able to make requests with the access token - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 200, channel.result) - - # time passes - self.reactor.advance(24 * 3600) - - # ... and we should be soft-logouted - channel = self.make_request(b"GET", TEST_URL, access_token=access_token) - self.assertEquals(channel.code, 401, channel.result) - self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN") - self.assertEquals(channel.json_body["soft_logout"], True) - - # Now try to hard log out all of the user's sessions - channel = self.make_request(b"POST", "/logout/all", access_token=access_token) - self.assertEquals(channel.result["code"], b"200", channel.result) - - -@skip_unless(has_saml2 and HAS_OIDC, "Requires SAML2 and OIDC") -class MultiSSOTestCase(unittest.HomeserverTestCase): - """Tests for homeservers with multiple SSO providers enabled""" - - servlets = [ - login.register_servlets, - ] - - def default_config(self) -> Dict[str, Any]: - config = super().default_config() - - config["public_baseurl"] = BASE_URL - - config["cas_config"] = { - "enabled": True, - "server_url": CAS_SERVER, - "service_url": "https://matrix.goodserver.com:8448", - } - - config["saml2_config"] = { - "sp_config": { - "metadata": {"inline": [TEST_SAML_METADATA]}, - # use the XMLSecurity backend to avoid relying on xmlsec1 - "crypto_backend": "XMLSecurity", - }, - } - - # default OIDC provider - config["oidc_config"] = TEST_OIDC_CONFIG - - # additional OIDC providers - config["oidc_providers"] = [ - { - "idp_id": "idp1", - "idp_name": "IDP1", - "discover": False, - "issuer": "https://issuer1", - "client_id": "test-client-id", - "client_secret": "test-client-secret", - "scopes": ["profile"], - "authorization_endpoint": "https://issuer1/auth", - "token_endpoint": "https://issuer1/token", - "userinfo_endpoint": "https://issuer1/userinfo", - "user_mapping_provider": { - "config": {"localpart_template": "{{ user.sub }}"} - }, - } - ] - return config - - def create_resource_dict(self) -> Dict[str, Resource]: - d = super().create_resource_dict() - d.update(build_synapse_client_resource_tree(self.hs)) - return d - - def test_get_login_flows(self): - """GET /login should return password and SSO flows""" - channel = self.make_request("GET", "/_matrix/client/r0/login") - self.assertEqual(channel.code, 200, channel.result) - - expected_flow_types = [ - "m.login.cas", - "m.login.sso", - "m.login.token", - "m.login.password", - ] + [f["type"] for f in ADDITIONAL_LOGIN_FLOWS] - - self.assertCountEqual( - [f["type"] for f in channel.json_body["flows"]], expected_flow_types - ) - - @override_config({"experimental_features": {"msc2858_enabled": True}}) - def test_get_msc2858_login_flows(self): - """The SSO flow should include IdP info if MSC2858 is enabled""" - channel = self.make_request("GET", "/_matrix/client/r0/login") - self.assertEqual(channel.code, 200, channel.result) - - # stick the flows results in a dict by type - flow_results: Dict[str, Any] = {} - for f in channel.json_body["flows"]: - flow_type = f["type"] - self.assertNotIn( - flow_type, flow_results, "duplicate flow type %s" % (flow_type,) - ) - flow_results[flow_type] = f - - self.assertIn("m.login.sso", flow_results, "m.login.sso was not returned") - sso_flow = flow_results.pop("m.login.sso") - # we should have a set of IdPs - self.assertCountEqual( - sso_flow["org.matrix.msc2858.identity_providers"], - [ - {"id": "cas", "name": "CAS"}, - {"id": "saml", "name": "SAML"}, - {"id": "oidc-idp1", "name": "IDP1"}, - {"id": "oidc", "name": "OIDC"}, - ], - ) - - # the rest of the flows are simple - expected_flows = [ - {"type": "m.login.cas"}, - {"type": "m.login.token"}, - {"type": "m.login.password"}, - ] + ADDITIONAL_LOGIN_FLOWS - - self.assertCountEqual(flow_results.values(), expected_flows) - - def test_multi_sso_redirect(self): - """/login/sso/redirect should redirect to an identity picker""" - # first hit the redirect url, which should redirect to our idp picker - channel = self._make_sso_redirect_request(False, None) - self.assertEqual(channel.code, 302, channel.result) - uri = channel.headers.getRawHeaders("Location")[0] - - # hitting that picker should give us some HTML - channel = self.make_request("GET", uri) - self.assertEqual(channel.code, 200, channel.result) - - # parse the form to check it has fields assumed elsewhere in this class - html = channel.result["body"].decode("utf-8") - p = TestHtmlParser() - p.feed(html) - p.close() - - # there should be a link for each href - returned_idps: List[str] = [] - for link in p.links: - path, query = link.split("?", 1) - self.assertEqual(path, "pick_idp") - params = urllib.parse.parse_qs(query) - self.assertEqual(params["redirectUrl"], [TEST_CLIENT_REDIRECT_URL]) - returned_idps.append(params["idp"][0]) - - self.assertCountEqual(returned_idps, ["cas", "oidc", "oidc-idp1", "saml"]) - - def test_multi_sso_redirect_to_cas(self): - """If CAS is chosen, should redirect to the CAS server""" - - channel = self.make_request( - "GET", - "/_synapse/client/pick_idp?redirectUrl=" - + urllib.parse.quote_plus(TEST_CLIENT_REDIRECT_URL) - + "&idp=cas", - shorthand=False, - ) - self.assertEqual(channel.code, 302, channel.result) - location_headers = channel.headers.getRawHeaders("Location") - assert location_headers - cas_uri = location_headers[0] - cas_uri_path, cas_uri_query = cas_uri.split("?", 1) - - # it should redirect us to the login page of the cas server - self.assertEqual(cas_uri_path, CAS_SERVER + "/login") - - # check that the redirectUrl is correctly encoded in the service param - ie, the - # place that CAS will redirect to - cas_uri_params = urllib.parse.parse_qs(cas_uri_query) - service_uri = cas_uri_params["service"][0] - _, service_uri_query = service_uri.split("?", 1) - service_uri_params = urllib.parse.parse_qs(service_uri_query) - self.assertEqual(service_uri_params["redirectUrl"][0], TEST_CLIENT_REDIRECT_URL) - - def test_multi_sso_redirect_to_saml(self): - """If SAML is chosen, should redirect to the SAML server""" - channel = self.make_request( - "GET", - "/_synapse/client/pick_idp?redirectUrl=" - + urllib.parse.quote_plus(TEST_CLIENT_REDIRECT_URL) - + "&idp=saml", - ) - self.assertEqual(channel.code, 302, channel.result) - location_headers = channel.headers.getRawHeaders("Location") - assert location_headers - saml_uri = location_headers[0] - saml_uri_path, saml_uri_query = saml_uri.split("?", 1) - - # it should redirect us to the login page of the SAML server - self.assertEqual(saml_uri_path, SAML_SERVER) - - # the RelayState is used to carry the client redirect url - saml_uri_params = urllib.parse.parse_qs(saml_uri_query) - relay_state_param = saml_uri_params["RelayState"][0] - self.assertEqual(relay_state_param, TEST_CLIENT_REDIRECT_URL) - - def test_login_via_oidc(self): - """If OIDC is chosen, should redirect to the OIDC auth endpoint""" - - # pick the default OIDC provider - channel = self.make_request( - "GET", - "/_synapse/client/pick_idp?redirectUrl=" - + urllib.parse.quote_plus(TEST_CLIENT_REDIRECT_URL) - + "&idp=oidc", - ) - self.assertEqual(channel.code, 302, channel.result) - location_headers = channel.headers.getRawHeaders("Location") - assert location_headers - oidc_uri = location_headers[0] - oidc_uri_path, oidc_uri_query = oidc_uri.split("?", 1) - - # it should redirect us to the auth page of the OIDC server - self.assertEqual(oidc_uri_path, TEST_OIDC_AUTH_ENDPOINT) - - # ... and should have set a cookie including the redirect url - cookie_headers = channel.headers.getRawHeaders("Set-Cookie") - assert cookie_headers - cookies: Dict[str, str] = {} - for h in cookie_headers: - key, value = h.split(";")[0].split("=", maxsplit=1) - cookies[key] = value - - oidc_session_cookie = cookies["oidc_session"] - macaroon = pymacaroons.Macaroon.deserialize(oidc_session_cookie) - self.assertEqual( - self._get_value_from_macaroon(macaroon, "client_redirect_url"), - TEST_CLIENT_REDIRECT_URL, - ) - - channel = self.helper.complete_oidc_auth(oidc_uri, cookies, {"sub": "user1"}) - - # that should serve a confirmation page - self.assertEqual(channel.code, 200, channel.result) - content_type_headers = channel.headers.getRawHeaders("Content-Type") - assert content_type_headers - self.assertTrue(content_type_headers[-1].startswith("text/html")) - p = TestHtmlParser() - p.feed(channel.text_body) - p.close() - - # ... which should contain our redirect link - self.assertEqual(len(p.links), 1) - path, query = p.links[0].split("?", 1) - self.assertEqual(path, "https://x") - - # it will have url-encoded the params properly, so we'll have to parse them - params = urllib.parse.parse_qsl( - query, keep_blank_values=True, strict_parsing=True, errors="strict" - ) - self.assertEqual(params[0:2], EXPECTED_CLIENT_REDIRECT_URL_PARAMS) - self.assertEqual(params[2][0], "loginToken") - - # finally, submit the matrix login token to the login API, which gives us our - # matrix access token, mxid, and device id. - login_token = params[2][1] - chan = self.make_request( - "POST", - "/login", - content={"type": "m.login.token", "token": login_token}, - ) - self.assertEqual(chan.code, 200, chan.result) - self.assertEqual(chan.json_body["user_id"], "@user1:test") - - def test_multi_sso_redirect_to_unknown(self): - """An unknown IdP should cause a 400""" - channel = self.make_request( - "GET", - "/_synapse/client/pick_idp?redirectUrl=http://x&idp=xyz", - ) - self.assertEqual(channel.code, 400, channel.result) - - def test_client_idp_redirect_to_unknown(self): - """If the client tries to pick an unknown IdP, return a 404""" - channel = self._make_sso_redirect_request(False, "xxx") - self.assertEqual(channel.code, 404, channel.result) - self.assertEqual(channel.json_body["errcode"], "M_NOT_FOUND") - - def test_client_idp_redirect_to_oidc(self): - """If the client pick a known IdP, redirect to it""" - channel = self._make_sso_redirect_request(False, "oidc") - self.assertEqual(channel.code, 302, channel.result) - oidc_uri = channel.headers.getRawHeaders("Location")[0] - oidc_uri_path, oidc_uri_query = oidc_uri.split("?", 1) - - # it should redirect us to the auth page of the OIDC server - self.assertEqual(oidc_uri_path, TEST_OIDC_AUTH_ENDPOINT) - - @override_config({"experimental_features": {"msc2858_enabled": True}}) - def test_client_msc2858_redirect_to_oidc(self): - """Test the unstable API""" - channel = self._make_sso_redirect_request(True, "oidc") - self.assertEqual(channel.code, 302, channel.result) - oidc_uri = channel.headers.getRawHeaders("Location")[0] - oidc_uri_path, oidc_uri_query = oidc_uri.split("?", 1) - - # it should redirect us to the auth page of the OIDC server - self.assertEqual(oidc_uri_path, TEST_OIDC_AUTH_ENDPOINT) - - def test_client_idp_redirect_msc2858_disabled(self): - """If the client tries to use the MSC2858 endpoint but MSC2858 is disabled, return a 400""" - channel = self._make_sso_redirect_request(True, "oidc") - self.assertEqual(channel.code, 400, channel.result) - self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED") - - def _make_sso_redirect_request( - self, unstable_endpoint: bool = False, idp_prov: Optional[str] = None - ): - """Send a request to /_matrix/client/r0/login/sso/redirect - - ... or the unstable equivalent - - ... possibly specifying an IDP provider - """ - endpoint = ( - "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" - if unstable_endpoint - else "/_matrix/client/r0/login/sso/redirect" - ) - if idp_prov is not None: - endpoint += "/" + idp_prov - endpoint += "?redirectUrl=" + urllib.parse.quote_plus(TEST_CLIENT_REDIRECT_URL) - - return self.make_request( - "GET", - endpoint, - custom_headers=[("Host", SYNAPSE_SERVER_PUBLIC_HOSTNAME)], - ) - - @staticmethod - def _get_value_from_macaroon(macaroon: pymacaroons.Macaroon, key: str) -> str: - 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,)) - - -class CASTestCase(unittest.HomeserverTestCase): - - servlets = [ - login.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - self.base_url = "https://matrix.goodserver.com/" - self.redirect_path = "_synapse/client/login/sso/redirect/confirm" - - config = self.default_config() - config["public_baseurl"] = ( - config.get("public_baseurl") or "https://matrix.goodserver.com:8448" - ) - config["cas_config"] = { - "enabled": True, - "server_url": CAS_SERVER, - } - - cas_user_id = "username" - self.user_id = "@%s:test" % cas_user_id - - async def get_raw(uri, args): - """Return an example response payload from a call to the `/proxyValidate` - endpoint of a CAS server, copied from - https://apereo.github.io/cas/5.0.x/protocol/CAS-Protocol-V2-Specification.html#26-proxyvalidate-cas-20 - - This needs to be returned by an async function (as opposed to set as the - mock's return value) because the corresponding Synapse code awaits on it. - """ - return ( - """ - <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> - <cas:authenticationSuccess> - <cas:user>%s</cas:user> - <cas:proxyGrantingTicket>PGTIOU-84678-8a9d...</cas:proxyGrantingTicket> - <cas:proxies> - <cas:proxy>https://proxy2/pgtUrl</cas:proxy> - <cas:proxy>https://proxy1/pgtUrl</cas:proxy> - </cas:proxies> - </cas:authenticationSuccess> - </cas:serviceResponse> - """ - % cas_user_id - ).encode("utf-8") - - mocked_http_client = Mock(spec=["get_raw"]) - mocked_http_client.get_raw.side_effect = get_raw - - self.hs = self.setup_test_homeserver( - config=config, - proxied_http_client=mocked_http_client, - ) - - return self.hs - - def prepare(self, reactor, clock, hs): - self.deactivate_account_handler = hs.get_deactivate_account_handler() - - def test_cas_redirect_confirm(self): - """Tests that the SSO login flow serves a confirmation page before redirecting a - user to the redirect URL. - """ - base_url = "/_matrix/client/r0/login/cas/ticket?redirectUrl" - redirect_url = "https://dodgy-site.com/" - - url_parts = list(urllib.parse.urlparse(base_url)) - query = dict(urllib.parse.parse_qsl(url_parts[4])) - query.update({"redirectUrl": redirect_url}) - query.update({"ticket": "ticket"}) - url_parts[4] = urllib.parse.urlencode(query) - cas_ticket_url = urllib.parse.urlunparse(url_parts) - - # Get Synapse to call the fake CAS and serve the template. - channel = self.make_request("GET", cas_ticket_url) - - # Test that the response is HTML. - self.assertEqual(channel.code, 200, channel.result) - content_type_header_value = "" - for header in channel.result.get("headers", []): - if header[0] == b"Content-Type": - content_type_header_value = header[1].decode("utf8") - - self.assertTrue(content_type_header_value.startswith("text/html")) - - # Test that the body isn't empty. - self.assertTrue(len(channel.result["body"]) > 0) - - # And that it contains our redirect link - self.assertIn(redirect_url, channel.result["body"].decode("UTF-8")) - - @override_config( - { - "sso": { - "client_whitelist": [ - "https://legit-site.com/", - "https://other-site.com/", - ] - } - } - ) - def test_cas_redirect_whitelisted(self): - """Tests that the SSO login flow serves a redirect to a whitelisted url""" - self._test_redirect("https://legit-site.com/") - - @override_config({"public_baseurl": "https://example.com"}) - def test_cas_redirect_login_fallback(self): - self._test_redirect("https://example.com/_matrix/static/client/login") - - def _test_redirect(self, redirect_url): - """Tests that the SSO login flow serves a redirect for the given redirect URL.""" - cas_ticket_url = ( - "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" - % (urllib.parse.quote(redirect_url)) - ) - - # Get Synapse to call the fake CAS and serve the template. - channel = self.make_request("GET", cas_ticket_url) - - self.assertEqual(channel.code, 302) - location_headers = channel.headers.getRawHeaders("Location") - assert location_headers - self.assertEqual(location_headers[0][: len(redirect_url)], redirect_url) - - @override_config({"sso": {"client_whitelist": ["https://legit-site.com/"]}}) - def test_deactivated_user(self): - """Logging in as a deactivated account should error.""" - redirect_url = "https://legit-site.com/" - - # First login (to create the user). - self._test_redirect(redirect_url) - - # Deactivate the account. - self.get_success( - self.deactivate_account_handler.deactivate_account( - self.user_id, False, create_requester(self.user_id) - ) - ) - - # Request the CAS ticket. - cas_ticket_url = ( - "/_matrix/client/r0/login/cas/ticket?redirectUrl=%s&ticket=ticket" - % (urllib.parse.quote(redirect_url)) - ) - - # Get Synapse to call the fake CAS and serve the template. - channel = self.make_request("GET", cas_ticket_url) - - # Because the user is deactivated they are served an error template. - self.assertEqual(channel.code, 403) - self.assertIn(b"SSO account deactivated", channel.result["body"]) - - -@skip_unless(HAS_JWT, "requires jwt") -class JWTTestCase(unittest.HomeserverTestCase): - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - login.register_servlets, - ] - - jwt_secret = "secret" - jwt_algorithm = "HS256" - - def make_homeserver(self, reactor, clock): - self.hs = self.setup_test_homeserver() - self.hs.config.jwt_enabled = True - self.hs.config.jwt_secret = self.jwt_secret - self.hs.config.jwt_algorithm = self.jwt_algorithm - return self.hs - - def jwt_encode(self, payload: Dict[str, Any], secret: str = jwt_secret) -> str: - # PyJWT 2.0.0 changed the return type of jwt.encode from bytes to str. - result: Union[str, bytes] = jwt.encode(payload, secret, self.jwt_algorithm) - if isinstance(result, bytes): - return result.decode("ascii") - return result - - def jwt_login(self, *args): - params = {"type": "org.matrix.login.jwt", "token": self.jwt_encode(*args)} - channel = self.make_request(b"POST", LOGIN_URL, params) - return channel - - def test_login_jwt_valid_registered(self): - self.register_user("kermit", "monkey") - channel = self.jwt_login({"sub": "kermit"}) - self.assertEqual(channel.result["code"], b"200", channel.result) - self.assertEqual(channel.json_body["user_id"], "@kermit:test") - - def test_login_jwt_valid_unregistered(self): - channel = self.jwt_login({"sub": "frog"}) - self.assertEqual(channel.result["code"], b"200", channel.result) - self.assertEqual(channel.json_body["user_id"], "@frog:test") - - def test_login_jwt_invalid_signature(self): - channel = self.jwt_login({"sub": "frog"}, "notsecret") - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], - "JWT validation failed: Signature verification failed", - ) - - def test_login_jwt_expired(self): - channel = self.jwt_login({"sub": "frog", "exp": 864000}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], "JWT validation failed: Signature has expired" - ) - - def test_login_jwt_not_before(self): - now = int(time.time()) - channel = self.jwt_login({"sub": "frog", "nbf": now + 3600}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], - "JWT validation failed: The token is not yet valid (nbf)", - ) - - def test_login_no_sub(self): - channel = self.jwt_login({"username": "root"}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual(channel.json_body["error"], "Invalid JWT") - - @override_config( - { - "jwt_config": { - "jwt_enabled": True, - "secret": jwt_secret, - "algorithm": jwt_algorithm, - "issuer": "test-issuer", - } - } - ) - def test_login_iss(self): - """Test validating the issuer claim.""" - # A valid issuer. - channel = self.jwt_login({"sub": "kermit", "iss": "test-issuer"}) - self.assertEqual(channel.result["code"], b"200", channel.result) - self.assertEqual(channel.json_body["user_id"], "@kermit:test") - - # An invalid issuer. - channel = self.jwt_login({"sub": "kermit", "iss": "invalid"}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], "JWT validation failed: Invalid issuer" - ) - - # Not providing an issuer. - channel = self.jwt_login({"sub": "kermit"}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], - 'JWT validation failed: Token is missing the "iss" claim', - ) - - def test_login_iss_no_config(self): - """Test providing an issuer claim without requiring it in the configuration.""" - channel = self.jwt_login({"sub": "kermit", "iss": "invalid"}) - self.assertEqual(channel.result["code"], b"200", channel.result) - self.assertEqual(channel.json_body["user_id"], "@kermit:test") - - @override_config( - { - "jwt_config": { - "jwt_enabled": True, - "secret": jwt_secret, - "algorithm": jwt_algorithm, - "audiences": ["test-audience"], - } - } - ) - def test_login_aud(self): - """Test validating the audience claim.""" - # A valid audience. - channel = self.jwt_login({"sub": "kermit", "aud": "test-audience"}) - self.assertEqual(channel.result["code"], b"200", channel.result) - self.assertEqual(channel.json_body["user_id"], "@kermit:test") - - # An invalid audience. - channel = self.jwt_login({"sub": "kermit", "aud": "invalid"}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], "JWT validation failed: Invalid audience" - ) - - # Not providing an audience. - channel = self.jwt_login({"sub": "kermit"}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], - 'JWT validation failed: Token is missing the "aud" claim', - ) - - def test_login_aud_no_config(self): - """Test providing an audience without requiring it in the configuration.""" - channel = self.jwt_login({"sub": "kermit", "aud": "invalid"}) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], "JWT validation failed: Invalid audience" - ) - - def test_login_no_token(self): - params = {"type": "org.matrix.login.jwt"} - channel = self.make_request(b"POST", LOGIN_URL, params) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual(channel.json_body["error"], "Token field for JWT is missing") - - -# The JWTPubKeyTestCase is a complement to JWTTestCase where we instead use -# RSS256, with a public key configured in synapse as "jwt_secret", and tokens -# signed by the private key. -@skip_unless(HAS_JWT, "requires jwt") -class JWTPubKeyTestCase(unittest.HomeserverTestCase): - servlets = [ - login.register_servlets, - ] - - # This key's pubkey is used as the jwt_secret setting of synapse. Valid - # tokens are signed by this and validated using the pubkey. It is generated - # with `openssl genrsa 512` (not a secure way to generate real keys, but - # good enough for tests!) - jwt_privatekey = "\n".join( - [ - "-----BEGIN RSA PRIVATE KEY-----", - "MIIBPAIBAAJBAM50f1Q5gsdmzifLstzLHb5NhfajiOt7TKO1vSEWdq7u9x8SMFiB", - "492RM9W/XFoh8WUfL9uL6Now6tPRDsWv3xsCAwEAAQJAUv7OOSOtiU+wzJq82rnk", - "yR4NHqt7XX8BvkZPM7/+EjBRanmZNSp5kYZzKVaZ/gTOM9+9MwlmhidrUOweKfB/", - "kQIhAPZwHazbjo7dYlJs7wPQz1vd+aHSEH+3uQKIysebkmm3AiEA1nc6mDdmgiUq", - "TpIN8A4MBKmfZMWTLq6z05y/qjKyxb0CIQDYJxCwTEenIaEa4PdoJl+qmXFasVDN", - "ZU0+XtNV7yul0wIhAMI9IhiStIjS2EppBa6RSlk+t1oxh2gUWlIh+YVQfZGRAiEA", - "tqBR7qLZGJ5CVKxWmNhJZGt1QHoUtOch8t9C4IdOZ2g=", - "-----END RSA PRIVATE KEY-----", - ] - ) - - # Generated with `openssl rsa -in foo.key -pubout`, with the the above - # private key placed in foo.key (jwt_privatekey). - jwt_pubkey = "\n".join( - [ - "-----BEGIN PUBLIC KEY-----", - "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM50f1Q5gsdmzifLstzLHb5NhfajiOt7", - "TKO1vSEWdq7u9x8SMFiB492RM9W/XFoh8WUfL9uL6Now6tPRDsWv3xsCAwEAAQ==", - "-----END PUBLIC KEY-----", - ] - ) - - # This key is used to sign tokens that shouldn't be accepted by synapse. - # Generated just like jwt_privatekey. - bad_privatekey = "\n".join( - [ - "-----BEGIN RSA PRIVATE KEY-----", - "MIIBOgIBAAJBAL//SQrKpKbjCCnv/FlasJCv+t3k/MPsZfniJe4DVFhsktF2lwQv", - "gLjmQD3jBUTz+/FndLSBvr3F4OHtGL9O/osCAwEAAQJAJqH0jZJW7Smzo9ShP02L", - "R6HRZcLExZuUrWI+5ZSP7TaZ1uwJzGFspDrunqaVoPobndw/8VsP8HFyKtceC7vY", - "uQIhAPdYInDDSJ8rFKGiy3Ajv5KWISBicjevWHF9dbotmNO9AiEAxrdRJVU+EI9I", - "eB4qRZpY6n4pnwyP0p8f/A3NBaQPG+cCIFlj08aW/PbxNdqYoBdeBA0xDrXKfmbb", - "iwYxBkwL0JCtAiBYmsi94sJn09u2Y4zpuCbJeDPKzWkbuwQh+W1fhIWQJQIhAKR0", - "KydN6cRLvphNQ9c/vBTdlzWxzcSxREpguC7F1J1m", - "-----END RSA PRIVATE KEY-----", - ] - ) - - def make_homeserver(self, reactor, clock): - self.hs = self.setup_test_homeserver() - self.hs.config.jwt_enabled = True - self.hs.config.jwt_secret = self.jwt_pubkey - self.hs.config.jwt_algorithm = "RS256" - return self.hs - - def jwt_encode(self, payload: Dict[str, Any], secret: str = jwt_privatekey) -> str: - # PyJWT 2.0.0 changed the return type of jwt.encode from bytes to str. - result: Union[bytes, str] = jwt.encode(payload, secret, "RS256") - if isinstance(result, bytes): - return result.decode("ascii") - return result - - def jwt_login(self, *args): - params = {"type": "org.matrix.login.jwt", "token": self.jwt_encode(*args)} - channel = self.make_request(b"POST", LOGIN_URL, params) - return channel - - def test_login_jwt_valid(self): - channel = self.jwt_login({"sub": "kermit"}) - self.assertEqual(channel.result["code"], b"200", channel.result) - self.assertEqual(channel.json_body["user_id"], "@kermit:test") - - def test_login_jwt_invalid_signature(self): - channel = self.jwt_login({"sub": "frog"}, self.bad_privatekey) - self.assertEqual(channel.result["code"], b"403", channel.result) - self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN") - self.assertEqual( - channel.json_body["error"], - "JWT validation failed: Signature verification failed", - ) - - -AS_USER = "as_user_alice" - - -class AppserviceLoginRestServletTestCase(unittest.HomeserverTestCase): - servlets = [ - login.register_servlets, - register.register_servlets, - ] - - def register_as_user(self, username): - self.make_request( - b"POST", - "/_matrix/client/r0/register?access_token=%s" % (self.service.token,), - {"username": username}, - ) - - def make_homeserver(self, reactor, clock): - self.hs = self.setup_test_homeserver() - - self.service = ApplicationService( - id="unique_identifier", - token="some_token", - hostname="example.com", - sender="@asbot:example.com", - namespaces={ - ApplicationService.NS_USERS: [ - {"regex": r"@as_user.*", "exclusive": False} - ], - ApplicationService.NS_ROOMS: [], - ApplicationService.NS_ALIASES: [], - }, - ) - self.another_service = ApplicationService( - id="another__identifier", - token="another_token", - hostname="example.com", - sender="@as2bot:example.com", - namespaces={ - ApplicationService.NS_USERS: [ - {"regex": r"@as2_user.*", "exclusive": False} - ], - ApplicationService.NS_ROOMS: [], - ApplicationService.NS_ALIASES: [], - }, - ) - - self.hs.get_datastore().services_cache.append(self.service) - self.hs.get_datastore().services_cache.append(self.another_service) - return self.hs - - def test_login_appservice_user(self): - """Test that an appservice user can use /login""" - self.register_as_user(AS_USER) - - params = { - "type": login.LoginRestServlet.APPSERVICE_TYPE, - "identifier": {"type": "m.id.user", "user": AS_USER}, - } - channel = self.make_request( - b"POST", LOGIN_URL, params, access_token=self.service.token - ) - - self.assertEquals(channel.result["code"], b"200", channel.result) - - def test_login_appservice_user_bot(self): - """Test that the appservice bot can use /login""" - self.register_as_user(AS_USER) - - params = { - "type": login.LoginRestServlet.APPSERVICE_TYPE, - "identifier": {"type": "m.id.user", "user": self.service.sender}, - } - channel = self.make_request( - b"POST", LOGIN_URL, params, access_token=self.service.token - ) - - self.assertEquals(channel.result["code"], b"200", channel.result) - - def test_login_appservice_wrong_user(self): - """Test that non-as users cannot login with the as token""" - self.register_as_user(AS_USER) - - params = { - "type": login.LoginRestServlet.APPSERVICE_TYPE, - "identifier": {"type": "m.id.user", "user": "fibble_wibble"}, - } - channel = self.make_request( - b"POST", LOGIN_URL, params, access_token=self.service.token - ) - - self.assertEquals(channel.result["code"], b"403", channel.result) - - def test_login_appservice_wrong_as(self): - """Test that as users cannot login with wrong as token""" - self.register_as_user(AS_USER) - - params = { - "type": login.LoginRestServlet.APPSERVICE_TYPE, - "identifier": {"type": "m.id.user", "user": AS_USER}, - } - channel = self.make_request( - b"POST", LOGIN_URL, params, access_token=self.another_service.token - ) - - self.assertEquals(channel.result["code"], b"403", channel.result) - - def test_login_appservice_no_token(self): - """Test that users must provide a token when using the appservice - login method - """ - self.register_as_user(AS_USER) - - params = { - "type": login.LoginRestServlet.APPSERVICE_TYPE, - "identifier": {"type": "m.id.user", "user": AS_USER}, - } - channel = self.make_request(b"POST", LOGIN_URL, params) - - self.assertEquals(channel.result["code"], b"401", channel.result) - - -@skip_unless(HAS_OIDC, "requires OIDC") -class UsernamePickerTestCase(HomeserverTestCase): - """Tests for the username picker flow of SSO login""" - - servlets = [login.register_servlets] - - def default_config(self): - config = super().default_config() - config["public_baseurl"] = BASE_URL - - config["oidc_config"] = {} - config["oidc_config"].update(TEST_OIDC_CONFIG) - config["oidc_config"]["user_mapping_provider"] = { - "config": {"display_name_template": "{{ user.displayname }}"} - } - - # whitelist this client URI so we redirect straight to it rather than - # serving a confirmation page - config["sso"] = {"client_whitelist": ["https://x"]} - return config - - def create_resource_dict(self) -> Dict[str, Resource]: - d = super().create_resource_dict() - d.update(build_synapse_client_resource_tree(self.hs)) - return d - - def test_username_picker(self): - """Test the happy path of a username picker flow.""" - - # do the start of the login flow - channel = self.helper.auth_via_oidc( - {"sub": "tester", "displayname": "Jonny"}, TEST_CLIENT_REDIRECT_URL - ) - - # that should redirect to the username picker - self.assertEqual(channel.code, 302, channel.result) - location_headers = channel.headers.getRawHeaders("Location") - assert location_headers - picker_url = location_headers[0] - self.assertEqual(picker_url, "/_synapse/client/pick_username/account_details") - - # ... with a username_mapping_session cookie - cookies: Dict[str, str] = {} - channel.extract_cookies(cookies) - self.assertIn("username_mapping_session", cookies) - session_id = cookies["username_mapping_session"] - - # introspect the sso handler a bit to check that the username mapping session - # looks ok. - username_mapping_sessions = self.hs.get_sso_handler()._username_mapping_sessions - self.assertIn( - session_id, - username_mapping_sessions, - "session id not found in map", - ) - session = username_mapping_sessions[session_id] - self.assertEqual(session.remote_user_id, "tester") - self.assertEqual(session.display_name, "Jonny") - self.assertEqual(session.client_redirect_url, TEST_CLIENT_REDIRECT_URL) - - # the expiry time should be about 15 minutes away - expected_expiry = self.clock.time_msec() + (15 * 60 * 1000) - self.assertApproximates(session.expiry_time_ms, expected_expiry, tolerance=1000) - - # Now, submit a username to the username picker, which should serve a redirect - # to the completion page - content = urlencode({b"username": b"bobby"}).encode("utf8") - chan = self.make_request( - "POST", - path=picker_url, - content=content, - content_is_form=True, - custom_headers=[ - ("Cookie", "username_mapping_session=" + session_id), - # old versions of twisted don't do form-parsing without a valid - # content-length header. - ("Content-Length", str(len(content))), - ], - ) - self.assertEqual(chan.code, 302, chan.result) - location_headers = chan.headers.getRawHeaders("Location") - assert location_headers - - # send a request to the completion page, which should 302 to the client redirectUrl - chan = self.make_request( - "GET", - path=location_headers[0], - custom_headers=[("Cookie", "username_mapping_session=" + session_id)], - ) - self.assertEqual(chan.code, 302, chan.result) - location_headers = chan.headers.getRawHeaders("Location") - assert location_headers - - # ensure that the returned location matches the requested redirect URL - path, query = location_headers[0].split("?", 1) - self.assertEqual(path, "https://x") - - # it will have url-encoded the params properly, so we'll have to parse them - params = urllib.parse.parse_qsl( - query, keep_blank_values=True, strict_parsing=True, errors="strict" - ) - self.assertEqual(params[0:2], EXPECTED_CLIENT_REDIRECT_URL_PARAMS) - self.assertEqual(params[2][0], "loginToken") - - # fish the login token out of the returned redirect uri - login_token = params[2][1] - - # finally, submit the matrix login token to the login API, which gives us our - # matrix access token, mxid, and device id. - chan = self.make_request( - "POST", - "/login", - content={"type": "m.login.token", "token": login_token}, - ) - self.assertEqual(chan.code, 200, chan.result) - self.assertEqual(chan.json_body["user_id"], "@bobby:test") diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py deleted file mode 100644 index 1d152352d1..0000000000 --- a/tests/rest/client/v1/test_presence.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2018 New Vector Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest.mock import Mock - -from twisted.internet import defer - -from synapse.handlers.presence import PresenceHandler -from synapse.rest.client import presence -from synapse.types import UserID - -from tests import unittest - - -class PresenceTestCase(unittest.HomeserverTestCase): - """Tests presence REST API.""" - - user_id = "@sid:red" - - user = UserID.from_string(user_id) - servlets = [presence.register_servlets] - - def make_homeserver(self, reactor, clock): - - presence_handler = Mock(spec=PresenceHandler) - presence_handler.set_state.return_value = defer.succeed(None) - - hs = self.setup_test_homeserver( - "red", - federation_http_client=None, - federation_client=Mock(), - presence_handler=presence_handler, - ) - - return hs - - def test_put_presence(self): - """ - PUT to the status endpoint with use_presence enabled will call - set_state on the presence handler. - """ - self.hs.config.use_presence = True - - body = {"presence": "here", "status_msg": "beep boop"} - channel = self.make_request( - "PUT", "/presence/%s/status" % (self.user_id,), body - ) - - self.assertEqual(channel.code, 200) - self.assertEqual(self.hs.get_presence_handler().set_state.call_count, 1) - - @unittest.override_config({"use_presence": False}) - def test_put_presence_disabled(self): - """ - PUT to the status endpoint with use_presence disabled will NOT call - set_state on the presence handler. - """ - - body = {"presence": "here", "status_msg": "beep boop"} - channel = self.make_request( - "PUT", "/presence/%s/status" % (self.user_id,), body - ) - - self.assertEqual(channel.code, 200) - self.assertEqual(self.hs.get_presence_handler().set_state.call_count, 0) diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py deleted file mode 100644 index 2860579c2e..0000000000 --- a/tests/rest/client/v1/test_profile.py +++ /dev/null @@ -1,270 +0,0 @@ -# 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. - -"""Tests REST events for /profile paths.""" -from synapse.rest import admin -from synapse.rest.client import login, profile, room - -from tests import unittest - - -class ProfileTestCase(unittest.HomeserverTestCase): - - servlets = [ - admin.register_servlets_for_client_rest_resource, - login.register_servlets, - profile.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - self.hs = self.setup_test_homeserver() - return self.hs - - def prepare(self, reactor, clock, hs): - self.owner = self.register_user("owner", "pass") - self.owner_tok = self.login("owner", "pass") - self.other = self.register_user("other", "pass", displayname="Bob") - - def test_get_displayname(self): - res = self._get_displayname() - self.assertEqual(res, "owner") - - def test_set_displayname(self): - channel = self.make_request( - "PUT", - "/profile/%s/displayname" % (self.owner,), - content={"displayname": "test"}, - access_token=self.owner_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - res = self._get_displayname() - self.assertEqual(res, "test") - - def test_set_displayname_noauth(self): - channel = self.make_request( - "PUT", - "/profile/%s/displayname" % (self.owner,), - content={"displayname": "test"}, - ) - self.assertEqual(channel.code, 401, channel.result) - - def test_set_displayname_too_long(self): - """Attempts to set a stupid displayname should get a 400""" - channel = self.make_request( - "PUT", - "/profile/%s/displayname" % (self.owner,), - content={"displayname": "test" * 100}, - access_token=self.owner_tok, - ) - self.assertEqual(channel.code, 400, channel.result) - - res = self._get_displayname() - self.assertEqual(res, "owner") - - def test_get_displayname_other(self): - res = self._get_displayname(self.other) - self.assertEquals(res, "Bob") - - def test_set_displayname_other(self): - channel = self.make_request( - "PUT", - "/profile/%s/displayname" % (self.other,), - content={"displayname": "test"}, - access_token=self.owner_tok, - ) - self.assertEqual(channel.code, 400, channel.result) - - def test_get_avatar_url(self): - res = self._get_avatar_url() - self.assertIsNone(res) - - def test_set_avatar_url(self): - channel = self.make_request( - "PUT", - "/profile/%s/avatar_url" % (self.owner,), - content={"avatar_url": "http://my.server/pic.gif"}, - access_token=self.owner_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - res = self._get_avatar_url() - self.assertEqual(res, "http://my.server/pic.gif") - - def test_set_avatar_url_noauth(self): - channel = self.make_request( - "PUT", - "/profile/%s/avatar_url" % (self.owner,), - content={"avatar_url": "http://my.server/pic.gif"}, - ) - self.assertEqual(channel.code, 401, channel.result) - - def test_set_avatar_url_too_long(self): - """Attempts to set a stupid avatar_url should get a 400""" - channel = self.make_request( - "PUT", - "/profile/%s/avatar_url" % (self.owner,), - content={"avatar_url": "http://my.server/pic.gif" * 100}, - access_token=self.owner_tok, - ) - self.assertEqual(channel.code, 400, channel.result) - - res = self._get_avatar_url() - self.assertIsNone(res) - - def test_get_avatar_url_other(self): - res = self._get_avatar_url(self.other) - self.assertIsNone(res) - - def test_set_avatar_url_other(self): - channel = self.make_request( - "PUT", - "/profile/%s/avatar_url" % (self.other,), - content={"avatar_url": "http://my.server/pic.gif"}, - access_token=self.owner_tok, - ) - self.assertEqual(channel.code, 400, channel.result) - - def _get_displayname(self, name=None): - channel = self.make_request( - "GET", "/profile/%s/displayname" % (name or self.owner,) - ) - self.assertEqual(channel.code, 200, channel.result) - return channel.json_body["displayname"] - - def _get_avatar_url(self, name=None): - channel = self.make_request( - "GET", "/profile/%s/avatar_url" % (name or self.owner,) - ) - self.assertEqual(channel.code, 200, channel.result) - return channel.json_body.get("avatar_url") - - -class ProfilesRestrictedTestCase(unittest.HomeserverTestCase): - - servlets = [ - admin.register_servlets_for_client_rest_resource, - login.register_servlets, - profile.register_servlets, - room.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - - config = self.default_config() - config["require_auth_for_profile_requests"] = True - config["limit_profile_requests_to_users_who_share_rooms"] = True - self.hs = self.setup_test_homeserver(config=config) - - return self.hs - - def prepare(self, reactor, clock, hs): - # User owning the requested profile. - self.owner = self.register_user("owner", "pass") - self.owner_tok = self.login("owner", "pass") - self.profile_url = "/profile/%s" % (self.owner) - - # User requesting the profile. - self.requester = self.register_user("requester", "pass") - self.requester_tok = self.login("requester", "pass") - - self.room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok) - - def test_no_auth(self): - self.try_fetch_profile(401) - - def test_not_in_shared_room(self): - self.ensure_requester_left_room() - - self.try_fetch_profile(403, access_token=self.requester_tok) - - def test_in_shared_room(self): - self.ensure_requester_left_room() - - self.helper.join(room=self.room_id, user=self.requester, tok=self.requester_tok) - - self.try_fetch_profile(200, self.requester_tok) - - def try_fetch_profile(self, expected_code, access_token=None): - self.request_profile(expected_code, access_token=access_token) - - self.request_profile( - expected_code, url_suffix="/displayname", access_token=access_token - ) - - self.request_profile( - expected_code, url_suffix="/avatar_url", access_token=access_token - ) - - def request_profile(self, expected_code, url_suffix="", access_token=None): - channel = self.make_request( - "GET", self.profile_url + url_suffix, access_token=access_token - ) - self.assertEqual(channel.code, expected_code, channel.result) - - def ensure_requester_left_room(self): - try: - self.helper.leave( - room=self.room_id, user=self.requester, tok=self.requester_tok - ) - except AssertionError: - # We don't care whether the leave request didn't return a 200 (e.g. - # if the user isn't already in the room), because we only want to - # make sure the user isn't in the room. - pass - - -class OwnProfileUnrestrictedTestCase(unittest.HomeserverTestCase): - - servlets = [ - admin.register_servlets_for_client_rest_resource, - login.register_servlets, - profile.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - config = self.default_config() - config["require_auth_for_profile_requests"] = True - config["limit_profile_requests_to_users_who_share_rooms"] = True - self.hs = self.setup_test_homeserver(config=config) - - return self.hs - - def prepare(self, reactor, clock, hs): - # User requesting the profile. - self.requester = self.register_user("requester", "pass") - self.requester_tok = self.login("requester", "pass") - - def test_can_lookup_own_profile(self): - """Tests that a user can lookup their own profile without having to be in a room - if 'require_auth_for_profile_requests' is set to true in the server's config. - """ - channel = self.make_request( - "GET", "/profile/" + self.requester, access_token=self.requester_tok - ) - self.assertEqual(channel.code, 200, channel.result) - - channel = self.make_request( - "GET", - "/profile/" + self.requester + "/displayname", - access_token=self.requester_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - channel = self.make_request( - "GET", - "/profile/" + self.requester + "/avatar_url", - access_token=self.requester_tok, - ) - self.assertEqual(channel.code, 200, channel.result) diff --git a/tests/rest/client/v1/test_push_rule_attrs.py b/tests/rest/client/v1/test_push_rule_attrs.py deleted file mode 100644 index d0ce91ccd9..0000000000 --- a/tests/rest/client/v1/test_push_rule_attrs.py +++ /dev/null @@ -1,414 +0,0 @@ -# Copyright 2020 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import synapse -from synapse.api.errors import Codes -from synapse.rest.client import login, push_rule, room - -from tests.unittest import HomeserverTestCase - - -class PushRuleAttributesTestCase(HomeserverTestCase): - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - push_rule.register_servlets, - ] - hijack_auth = False - - def test_enabled_on_creation(self): - """ - Tests the GET and PUT of push rules' `enabled` endpoints. - Tests that a rule is enabled upon creation, even though a rule with that - ruleId existed previously and was disabled. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # GET enabled for that new rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual(channel.json_body["enabled"], True) - - def test_enabled_on_recreation(self): - """ - Tests the GET and PUT of push rules' `enabled` endpoints. - Tests that a rule is enabled upon creation, even if a rule with that - ruleId existed previously and was disabled. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # disable the rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/best.friend/enabled", - {"enabled": False}, - access_token=token, - ) - self.assertEqual(channel.code, 200) - - # check rule disabled - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual(channel.json_body["enabled"], False) - - # DELETE the rule - channel = self.make_request( - "DELETE", "/pushrules/global/override/best.friend", access_token=token - ) - self.assertEqual(channel.code, 200) - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # GET enabled for that new rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual(channel.json_body["enabled"], True) - - def test_enabled_disable(self): - """ - Tests the GET and PUT of push rules' `enabled` endpoints. - Tests that a rule is disabled and enabled when we ask for it. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # disable the rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/best.friend/enabled", - {"enabled": False}, - access_token=token, - ) - self.assertEqual(channel.code, 200) - - # check rule disabled - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual(channel.json_body["enabled"], False) - - # re-enable the rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/best.friend/enabled", - {"enabled": True}, - access_token=token, - ) - self.assertEqual(channel.code, 200) - - # check rule enabled - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual(channel.json_body["enabled"], True) - - def test_enabled_404_when_get_non_existent(self): - """ - Tests that `enabled` gives 404 when the rule doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # check 404 for never-heard-of rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # GET enabled for that new rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 200) - - # DELETE the rule - channel = self.make_request( - "DELETE", "/pushrules/global/override/best.friend", access_token=token - ) - self.assertEqual(channel.code, 200) - - # check 404 for deleted rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_enabled_404_when_get_non_existent_server_rule(self): - """ - Tests that `enabled` gives 404 when the server-default rule doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - # check 404 for never-heard-of rule - channel = self.make_request( - "GET", "/pushrules/global/override/.m.muahahaha/enabled", access_token=token - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_enabled_404_when_put_non_existent_rule(self): - """ - Tests that `enabled` gives 404 when we put to a rule that doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - # enable & check 404 for never-heard-of rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/best.friend/enabled", - {"enabled": True}, - access_token=token, - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_enabled_404_when_put_non_existent_server_rule(self): - """ - Tests that `enabled` gives 404 when we put to a server-default rule that doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - # enable & check 404 for never-heard-of rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/.m.muahahah/enabled", - {"enabled": True}, - access_token=token, - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_actions_get(self): - """ - Tests that `actions` gives you what you expect on a fresh rule. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # GET actions for that new rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/actions", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual( - channel.json_body["actions"], ["notify", {"set_tweak": "highlight"}] - ) - - def test_actions_put(self): - """ - Tests that PUT on actions updates the value you'd get from GET. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # change the rule actions - channel = self.make_request( - "PUT", - "/pushrules/global/override/best.friend/actions", - {"actions": ["dont_notify"]}, - access_token=token, - ) - self.assertEqual(channel.code, 200) - - # GET actions for that new rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/actions", access_token=token - ) - self.assertEqual(channel.code, 200) - self.assertEqual(channel.json_body["actions"], ["dont_notify"]) - - def test_actions_404_when_get_non_existent(self): - """ - Tests that `actions` gives 404 when the rule doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - body = { - "conditions": [ - {"kind": "event_match", "key": "sender", "pattern": "@user2:hs"} - ], - "actions": ["notify", {"set_tweak": "highlight"}], - } - - # check 404 for never-heard-of rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - # PUT a new rule - channel = self.make_request( - "PUT", "/pushrules/global/override/best.friend", body, access_token=token - ) - self.assertEqual(channel.code, 200) - - # DELETE the rule - channel = self.make_request( - "DELETE", "/pushrules/global/override/best.friend", access_token=token - ) - self.assertEqual(channel.code, 200) - - # check 404 for deleted rule - channel = self.make_request( - "GET", "/pushrules/global/override/best.friend/enabled", access_token=token - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_actions_404_when_get_non_existent_server_rule(self): - """ - Tests that `actions` gives 404 when the server-default rule doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - # check 404 for never-heard-of rule - channel = self.make_request( - "GET", "/pushrules/global/override/.m.muahahaha/actions", access_token=token - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_actions_404_when_put_non_existent_rule(self): - """ - Tests that `actions` gives 404 when putting to a rule that doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - # enable & check 404 for never-heard-of rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/best.friend/actions", - {"actions": ["dont_notify"]}, - access_token=token, - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) - - def test_actions_404_when_put_non_existent_server_rule(self): - """ - Tests that `actions` gives 404 when putting to a server-default rule that doesn't exist. - """ - self.register_user("user", "pass") - token = self.login("user", "pass") - - # enable & check 404 for never-heard-of rule - channel = self.make_request( - "PUT", - "/pushrules/global/override/.m.muahahah/actions", - {"actions": ["dont_notify"]}, - access_token=token, - ) - self.assertEqual(channel.code, 404) - self.assertEqual(channel.json_body["errcode"], Codes.NOT_FOUND) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py deleted file mode 100644 index 0c9cbb9aff..0000000000 --- a/tests/rest/client/v1/test_rooms.py +++ /dev/null @@ -1,2150 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2017 Vector Creations Ltd -# Copyright 2018-2019 New Vector Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests REST events for /rooms paths.""" - -import json -from typing import Iterable -from unittest.mock import Mock, call -from urllib import parse as urlparse - -from twisted.internet import defer - -import synapse.rest.admin -from synapse.api.constants import EventContentFields, EventTypes, Membership -from synapse.api.errors import HttpResponseException -from synapse.handlers.pagination import PurgeStatus -from synapse.rest import admin -from synapse.rest.client import account, directory, login, profile, room -from synapse.types import JsonDict, RoomAlias, UserID, create_requester -from synapse.util.stringutils import random_string - -from tests import unittest -from tests.test_utils import make_awaitable - -PATH_PREFIX = b"/_matrix/client/api/v1" - - -class RoomBase(unittest.HomeserverTestCase): - rmcreator_id = None - - servlets = [room.register_servlets, room.register_deprecated_servlets] - - def make_homeserver(self, reactor, clock): - - self.hs = self.setup_test_homeserver( - "red", - federation_http_client=None, - federation_client=Mock(), - ) - - self.hs.get_federation_handler = Mock() - self.hs.get_federation_handler.return_value.maybe_backfill = Mock( - return_value=make_awaitable(None) - ) - - async def _insert_client_ip(*args, **kwargs): - return None - - self.hs.get_datastore().insert_client_ip = _insert_client_ip - - return self.hs - - -class RoomPermissionsTestCase(RoomBase): - """Tests room permissions.""" - - user_id = "@sid1:red" - rmcreator_id = "@notme:red" - - def prepare(self, reactor, clock, hs): - - self.helper.auth_user_id = self.rmcreator_id - # create some rooms under the name rmcreator_id - self.uncreated_rmid = "!aa:test" - self.created_rmid = self.helper.create_room_as( - self.rmcreator_id, is_public=False - ) - self.created_public_rmid = self.helper.create_room_as( - self.rmcreator_id, is_public=True - ) - - # send a message in one of the rooms - self.created_rmid_msg_path = ( - "rooms/%s/send/m.room.message/a1" % (self.created_rmid) - ).encode("ascii") - channel = self.make_request( - "PUT", self.created_rmid_msg_path, b'{"msgtype":"m.text","body":"test msg"}' - ) - self.assertEquals(200, channel.code, channel.result) - - # set topic for public room - channel = self.make_request( - "PUT", - ("rooms/%s/state/m.room.topic" % self.created_public_rmid).encode("ascii"), - b'{"topic":"Public Room Topic"}', - ) - self.assertEquals(200, channel.code, channel.result) - - # auth as user_id now - self.helper.auth_user_id = self.user_id - - def test_can_do_action(self): - msg_content = b'{"msgtype":"m.text","body":"hello"}' - - seq = iter(range(100)) - - def send_msg_path(): - return "/rooms/%s/send/m.room.message/mid%s" % ( - self.created_rmid, - str(next(seq)), - ) - - # send message in uncreated room, expect 403 - channel = self.make_request( - "PUT", - "/rooms/%s/send/m.room.message/mid2" % (self.uncreated_rmid,), - msg_content, - ) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # send message in created room not joined (no state), expect 403 - channel = self.make_request("PUT", send_msg_path(), msg_content) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # send message in created room and invited, expect 403 - self.helper.invite( - room=self.created_rmid, src=self.rmcreator_id, targ=self.user_id - ) - channel = self.make_request("PUT", send_msg_path(), msg_content) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # send message in created room and joined, expect 200 - self.helper.join(room=self.created_rmid, user=self.user_id) - channel = self.make_request("PUT", send_msg_path(), msg_content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - # send message in created room and left, expect 403 - self.helper.leave(room=self.created_rmid, user=self.user_id) - channel = self.make_request("PUT", send_msg_path(), msg_content) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - def test_topic_perms(self): - topic_content = b'{"topic":"My Topic Name"}' - topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid - - # set/get topic in uncreated room, expect 403 - channel = self.make_request( - "PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid, topic_content - ) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - channel = self.make_request( - "GET", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid - ) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # set/get topic in created PRIVATE room not joined, expect 403 - channel = self.make_request("PUT", topic_path, topic_content) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - channel = self.make_request("GET", topic_path) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # set topic in created PRIVATE room and invited, expect 403 - self.helper.invite( - room=self.created_rmid, src=self.rmcreator_id, targ=self.user_id - ) - channel = self.make_request("PUT", topic_path, topic_content) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # get topic in created PRIVATE room and invited, expect 403 - channel = self.make_request("GET", topic_path) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # set/get topic in created PRIVATE room and joined, expect 200 - self.helper.join(room=self.created_rmid, user=self.user_id) - - # Only room ops can set topic by default - self.helper.auth_user_id = self.rmcreator_id - channel = self.make_request("PUT", topic_path, topic_content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - self.helper.auth_user_id = self.user_id - - channel = self.make_request("GET", topic_path) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - self.assert_dict(json.loads(topic_content.decode("utf8")), channel.json_body) - - # set/get topic in created PRIVATE room and left, expect 403 - self.helper.leave(room=self.created_rmid, user=self.user_id) - channel = self.make_request("PUT", topic_path, topic_content) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - channel = self.make_request("GET", topic_path) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - # get topic in PUBLIC room, not joined, expect 403 - channel = self.make_request( - "GET", "/rooms/%s/state/m.room.topic" % self.created_public_rmid - ) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - # set topic in PUBLIC room, not joined, expect 403 - channel = self.make_request( - "PUT", - "/rooms/%s/state/m.room.topic" % self.created_public_rmid, - topic_content, - ) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - def _test_get_membership( - self, room=None, members: Iterable = frozenset(), expect_code=None - ): - for member in members: - path = "/rooms/%s/state/m.room.member/%s" % (room, member) - channel = self.make_request("GET", path) - self.assertEquals(expect_code, channel.code) - - def test_membership_basic_room_perms(self): - # === room does not exist === - room = self.uncreated_rmid - # get membership of self, get membership of other, uncreated room - # expect all 403s - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=403 - ) - - # trying to invite people to this room should 403 - self.helper.invite( - room=room, src=self.user_id, targ=self.rmcreator_id, expect_code=403 - ) - - # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 404s because room doesn't exist on any server - for usr in [self.user_id, self.rmcreator_id]: - self.helper.join(room=room, user=usr, expect_code=404) - self.helper.leave(room=room, user=usr, expect_code=404) - - def test_membership_private_room_perms(self): - room = self.created_rmid - # get membership of self, get membership of other, private room + invite - # expect all 403s - self.helper.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=403 - ) - - # get membership of self, get membership of other, private room + joined - # expect all 200s - self.helper.join(room=room, user=self.user_id) - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=200 - ) - - # get membership of self, get membership of other, private room + left - # expect all 200s - self.helper.leave(room=room, user=self.user_id) - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=200 - ) - - def test_membership_public_room_perms(self): - room = self.created_public_rmid - # get membership of self, get membership of other, public room + invite - # expect 403 - self.helper.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=403 - ) - - # get membership of self, get membership of other, public room + joined - # expect all 200s - self.helper.join(room=room, user=self.user_id) - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=200 - ) - - # get membership of self, get membership of other, public room + left - # expect 200. - self.helper.leave(room=room, user=self.user_id) - self._test_get_membership( - members=[self.user_id, self.rmcreator_id], room=room, expect_code=200 - ) - - def test_invited_permissions(self): - room = self.created_rmid - self.helper.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - - # set [invite/join/left] of other user, expect 403s - self.helper.invite( - room=room, src=self.user_id, targ=self.rmcreator_id, expect_code=403 - ) - self.helper.change_membership( - room=room, - src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.JOIN, - expect_code=403, - ) - self.helper.change_membership( - room=room, - src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.LEAVE, - expect_code=403, - ) - - def test_joined_permissions(self): - room = self.created_rmid - self.helper.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - self.helper.join(room=room, user=self.user_id) - - # set invited of self, expect 403 - self.helper.invite( - room=room, src=self.user_id, targ=self.user_id, expect_code=403 - ) - - # set joined of self, expect 200 (NOOP) - self.helper.join(room=room, user=self.user_id) - - other = "@burgundy:red" - # set invited of other, expect 200 - self.helper.invite(room=room, src=self.user_id, targ=other, expect_code=200) - - # set joined of other, expect 403 - self.helper.change_membership( - room=room, - src=self.user_id, - targ=other, - membership=Membership.JOIN, - expect_code=403, - ) - - # set left of other, expect 403 - self.helper.change_membership( - room=room, - src=self.user_id, - targ=other, - membership=Membership.LEAVE, - expect_code=403, - ) - - # set left of self, expect 200 - self.helper.leave(room=room, user=self.user_id) - - def test_leave_permissions(self): - room = self.created_rmid - self.helper.invite(room=room, src=self.rmcreator_id, targ=self.user_id) - self.helper.join(room=room, user=self.user_id) - self.helper.leave(room=room, user=self.user_id) - - # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 403s - for usr in [self.user_id, self.rmcreator_id]: - self.helper.change_membership( - room=room, - src=self.user_id, - targ=usr, - membership=Membership.INVITE, - expect_code=403, - ) - - self.helper.change_membership( - room=room, - src=self.user_id, - targ=usr, - membership=Membership.JOIN, - expect_code=403, - ) - - # It is always valid to LEAVE if you've already left (currently.) - self.helper.change_membership( - room=room, - src=self.user_id, - targ=self.rmcreator_id, - membership=Membership.LEAVE, - expect_code=403, - ) - - -class RoomsMemberListTestCase(RoomBase): - """Tests /rooms/$room_id/members/list REST events.""" - - user_id = "@sid1:red" - - def test_get_member_list(self): - room_id = self.helper.create_room_as(self.user_id) - channel = self.make_request("GET", "/rooms/%s/members" % room_id) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - def test_get_member_list_no_room(self): - channel = self.make_request("GET", "/rooms/roomdoesnotexist/members") - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - def test_get_member_list_no_permission(self): - room_id = self.helper.create_room_as("@some_other_guy:red") - channel = self.make_request("GET", "/rooms/%s/members" % room_id) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - def test_get_member_list_mixed_memberships(self): - room_creator = "@some_other_guy:red" - room_id = self.helper.create_room_as(room_creator) - room_path = "/rooms/%s/members" % room_id - self.helper.invite(room=room_id, src=room_creator, targ=self.user_id) - # can't see list if you're just invited. - channel = self.make_request("GET", room_path) - self.assertEquals(403, channel.code, msg=channel.result["body"]) - - self.helper.join(room=room_id, user=self.user_id) - # can see list now joined - channel = self.make_request("GET", room_path) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - self.helper.leave(room=room_id, user=self.user_id) - # can see old list once left - channel = self.make_request("GET", room_path) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - -class RoomsCreateTestCase(RoomBase): - """Tests /rooms and /rooms/$room_id REST events.""" - - user_id = "@sid1:red" - - def test_post_room_no_keys(self): - # POST with no config keys, expect new room id - channel = self.make_request("POST", "/createRoom", "{}") - - self.assertEquals(200, channel.code, channel.result) - self.assertTrue("room_id" in channel.json_body) - - def test_post_room_visibility_key(self): - # POST with visibility config key, expect new room id - channel = self.make_request("POST", "/createRoom", b'{"visibility":"private"}') - self.assertEquals(200, channel.code) - self.assertTrue("room_id" in channel.json_body) - - def test_post_room_custom_key(self): - # POST with custom config keys, expect new room id - channel = self.make_request("POST", "/createRoom", b'{"custom":"stuff"}') - self.assertEquals(200, channel.code) - self.assertTrue("room_id" in channel.json_body) - - def test_post_room_known_and_unknown_keys(self): - # POST with custom + known config keys, expect new room id - channel = self.make_request( - "POST", "/createRoom", b'{"visibility":"private","custom":"things"}' - ) - self.assertEquals(200, channel.code) - self.assertTrue("room_id" in channel.json_body) - - def test_post_room_invalid_content(self): - # POST with invalid content / paths, expect 400 - channel = self.make_request("POST", "/createRoom", b'{"visibili') - self.assertEquals(400, channel.code) - - channel = self.make_request("POST", "/createRoom", b'["hello"]') - self.assertEquals(400, channel.code) - - def test_post_room_invitees_invalid_mxid(self): - # POST with invalid invitee, see https://github.com/matrix-org/synapse/issues/4088 - # Note the trailing space in the MXID here! - channel = self.make_request( - "POST", "/createRoom", b'{"invite":["@alice:example.com "]}' - ) - self.assertEquals(400, channel.code) - - @unittest.override_config({"rc_invites": {"per_room": {"burst_count": 3}}}) - def test_post_room_invitees_ratelimit(self): - """Test that invites sent when creating a room are ratelimited by a RateLimiter, - which ratelimits them correctly, including by not limiting when the requester is - exempt from ratelimiting. - """ - - # Build the request's content. We use local MXIDs because invites over federation - # are more difficult to mock. - content = json.dumps( - { - "invite": [ - "@alice1:red", - "@alice2:red", - "@alice3:red", - "@alice4:red", - ] - } - ).encode("utf8") - - # Test that the invites are correctly ratelimited. - channel = self.make_request("POST", "/createRoom", content) - self.assertEqual(400, channel.code) - self.assertEqual( - "Cannot invite so many users at once", - channel.json_body["error"], - ) - - # Add the current user to the ratelimit overrides, allowing them no ratelimiting. - self.get_success( - self.hs.get_datastore().set_ratelimit_for_user(self.user_id, 0, 0) - ) - - # Test that the invites aren't ratelimited anymore. - channel = self.make_request("POST", "/createRoom", content) - self.assertEqual(200, channel.code) - - -class RoomTopicTestCase(RoomBase): - """Tests /rooms/$room_id/topic REST events.""" - - user_id = "@sid1:red" - - def prepare(self, reactor, clock, hs): - # create the room - self.room_id = self.helper.create_room_as(self.user_id) - self.path = "/rooms/%s/state/m.room.topic" % (self.room_id,) - - def test_invalid_puts(self): - # missing keys or invalid json - channel = self.make_request("PUT", self.path, "{}") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", self.path, '{"_name":"bo"}') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", self.path, '{"nao') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request( - "PUT", self.path, '[{"_name":"bo"},{"_name":"jill"}]' - ) - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", self.path, "text only") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", self.path, "") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - # valid key, wrong type - content = '{"topic":["Topic name"]}' - channel = self.make_request("PUT", self.path, content) - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - def test_rooms_topic(self): - # nothing should be there - channel = self.make_request("GET", self.path) - self.assertEquals(404, channel.code, msg=channel.result["body"]) - - # valid put - content = '{"topic":"Topic name"}' - channel = self.make_request("PUT", self.path, content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - # valid get - channel = self.make_request("GET", self.path) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - self.assert_dict(json.loads(content), channel.json_body) - - def test_rooms_topic_with_extra_keys(self): - # valid put with extra keys - content = '{"topic":"Seasons","subtopic":"Summer"}' - channel = self.make_request("PUT", self.path, content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - # valid get - channel = self.make_request("GET", self.path) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - self.assert_dict(json.loads(content), channel.json_body) - - -class RoomMemberStateTestCase(RoomBase): - """Tests /rooms/$room_id/members/$user_id/state REST events.""" - - user_id = "@sid1:red" - - def prepare(self, reactor, clock, hs): - self.room_id = self.helper.create_room_as(self.user_id) - - def test_invalid_puts(self): - path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id) - # missing keys or invalid json - channel = self.make_request("PUT", path, "{}") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, '{"_name":"bo"}') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, '{"nao') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, b'[{"_name":"bo"},{"_name":"jill"}]') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, "text only") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, "") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - # valid keys, wrong types - content = '{"membership":["%s","%s","%s"]}' % ( - Membership.INVITE, - Membership.JOIN, - Membership.LEAVE, - ) - channel = self.make_request("PUT", path, content.encode("ascii")) - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - def test_rooms_members_self(self): - path = "/rooms/%s/state/m.room.member/%s" % ( - urlparse.quote(self.room_id), - self.user_id, - ) - - # valid join message (NOOP since we made the room) - content = '{"membership":"%s"}' % Membership.JOIN - channel = self.make_request("PUT", path, content.encode("ascii")) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - channel = self.make_request("GET", path, None) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - expected_response = {"membership": Membership.JOIN} - self.assertEquals(expected_response, channel.json_body) - - def test_rooms_members_other(self): - self.other_id = "@zzsid1:red" - path = "/rooms/%s/state/m.room.member/%s" % ( - urlparse.quote(self.room_id), - self.other_id, - ) - - # valid invite message - content = '{"membership":"%s"}' % Membership.INVITE - channel = self.make_request("PUT", path, content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - channel = self.make_request("GET", path, None) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - self.assertEquals(json.loads(content), channel.json_body) - - def test_rooms_members_other_custom_keys(self): - self.other_id = "@zzsid1:red" - path = "/rooms/%s/state/m.room.member/%s" % ( - urlparse.quote(self.room_id), - self.other_id, - ) - - # valid invite message with custom key - content = '{"membership":"%s","invite_text":"%s"}' % ( - Membership.INVITE, - "Join us!", - ) - channel = self.make_request("PUT", path, content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - channel = self.make_request("GET", path, None) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - self.assertEquals(json.loads(content), channel.json_body) - - -class RoomInviteRatelimitTestCase(RoomBase): - user_id = "@sid1:red" - - servlets = [ - admin.register_servlets, - profile.register_servlets, - room.register_servlets, - ] - - @unittest.override_config( - {"rc_invites": {"per_room": {"per_second": 0.5, "burst_count": 3}}} - ) - def test_invites_by_rooms_ratelimit(self): - """Tests that invites in a room are actually rate-limited.""" - room_id = self.helper.create_room_as(self.user_id) - - for i in range(3): - self.helper.invite(room_id, self.user_id, "@user-%s:red" % (i,)) - - self.helper.invite(room_id, self.user_id, "@user-4:red", expect_code=429) - - @unittest.override_config( - {"rc_invites": {"per_user": {"per_second": 0.5, "burst_count": 3}}} - ) - def test_invites_by_users_ratelimit(self): - """Tests that invites to a specific user are actually rate-limited.""" - - for _ in range(3): - room_id = self.helper.create_room_as(self.user_id) - self.helper.invite(room_id, self.user_id, "@other-users:red") - - room_id = self.helper.create_room_as(self.user_id) - self.helper.invite(room_id, self.user_id, "@other-users:red", expect_code=429) - - -class RoomJoinRatelimitTestCase(RoomBase): - user_id = "@sid1:red" - - servlets = [ - admin.register_servlets, - profile.register_servlets, - room.register_servlets, - ] - - @unittest.override_config( - {"rc_joins": {"local": {"per_second": 0.5, "burst_count": 3}}} - ) - def test_join_local_ratelimit(self): - """Tests that local joins are actually rate-limited.""" - for _ in range(3): - self.helper.create_room_as(self.user_id) - - self.helper.create_room_as(self.user_id, expect_code=429) - - @unittest.override_config( - {"rc_joins": {"local": {"per_second": 0.5, "burst_count": 3}}} - ) - def test_join_local_ratelimit_profile_change(self): - """Tests that sending a profile update into all of the user's joined rooms isn't - rate-limited by the rate-limiter on joins.""" - - # Create and join as many rooms as the rate-limiting config allows in a second. - room_ids = [ - self.helper.create_room_as(self.user_id), - self.helper.create_room_as(self.user_id), - self.helper.create_room_as(self.user_id), - ] - # Let some time for the rate-limiter to forget about our multi-join. - self.reactor.advance(2) - # Add one to make sure we're joined to more rooms than the config allows us to - # join in a second. - room_ids.append(self.helper.create_room_as(self.user_id)) - - # Create a profile for the user, since it hasn't been done on registration. - store = self.hs.get_datastore() - self.get_success( - store.create_profile(UserID.from_string(self.user_id).localpart) - ) - - # Update the display name for the user. - path = "/_matrix/client/r0/profile/%s/displayname" % self.user_id - channel = self.make_request("PUT", path, {"displayname": "John Doe"}) - self.assertEquals(channel.code, 200, channel.json_body) - - # Check that all the rooms have been sent a profile update into. - for room_id in room_ids: - path = "/_matrix/client/r0/rooms/%s/state/m.room.member/%s" % ( - room_id, - self.user_id, - ) - - channel = self.make_request("GET", path) - self.assertEquals(channel.code, 200) - - self.assertIn("displayname", channel.json_body) - self.assertEquals(channel.json_body["displayname"], "John Doe") - - @unittest.override_config( - {"rc_joins": {"local": {"per_second": 0.5, "burst_count": 3}}} - ) - def test_join_local_ratelimit_idempotent(self): - """Tests that the room join endpoints remain idempotent despite rate-limiting - on room joins.""" - room_id = self.helper.create_room_as(self.user_id) - - # Let's test both paths to be sure. - paths_to_test = [ - "/_matrix/client/r0/rooms/%s/join", - "/_matrix/client/r0/join/%s", - ] - - for path in paths_to_test: - # Make sure we send more requests than the rate-limiting config would allow - # if all of these requests ended up joining the user to a room. - for _ in range(4): - channel = self.make_request("POST", path % room_id, {}) - self.assertEquals(channel.code, 200) - - @unittest.override_config( - { - "rc_joins": {"local": {"per_second": 0.5, "burst_count": 3}}, - "auto_join_rooms": ["#room:red", "#room2:red", "#room3:red", "#room4:red"], - "autocreate_auto_join_rooms": True, - }, - ) - def test_autojoin_rooms(self): - user_id = self.register_user("testuser", "password") - - # Check that the new user successfully joined the four rooms - rooms = self.get_success(self.hs.get_datastore().get_rooms_for_user(user_id)) - self.assertEqual(len(rooms), 4) - - -class RoomMessagesTestCase(RoomBase): - """Tests /rooms/$room_id/messages/$user_id/$msg_id REST events.""" - - user_id = "@sid1:red" - - def prepare(self, reactor, clock, hs): - self.room_id = self.helper.create_room_as(self.user_id) - - def test_invalid_puts(self): - path = "/rooms/%s/send/m.room.message/mid1" % (urlparse.quote(self.room_id)) - # missing keys or invalid json - channel = self.make_request("PUT", path, b"{}") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, b'{"_name":"bo"}') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, b'{"nao') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, b'[{"_name":"bo"},{"_name":"jill"}]') - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, b"text only") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - channel = self.make_request("PUT", path, b"") - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - def test_rooms_messages_sent(self): - path = "/rooms/%s/send/m.room.message/mid1" % (urlparse.quote(self.room_id)) - - content = b'{"body":"test","msgtype":{"type":"a"}}' - channel = self.make_request("PUT", path, content) - self.assertEquals(400, channel.code, msg=channel.result["body"]) - - # custom message types - content = b'{"body":"test","msgtype":"test.custom.text"}' - channel = self.make_request("PUT", path, content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - # m.text message type - path = "/rooms/%s/send/m.room.message/mid2" % (urlparse.quote(self.room_id)) - content = b'{"body":"test2","msgtype":"m.text"}' - channel = self.make_request("PUT", path, content) - self.assertEquals(200, channel.code, msg=channel.result["body"]) - - -class RoomInitialSyncTestCase(RoomBase): - """Tests /rooms/$room_id/initialSync.""" - - user_id = "@sid1:red" - - def prepare(self, reactor, clock, hs): - # create the room - self.room_id = self.helper.create_room_as(self.user_id) - - def test_initial_sync(self): - channel = self.make_request("GET", "/rooms/%s/initialSync" % self.room_id) - self.assertEquals(200, channel.code) - - self.assertEquals(self.room_id, channel.json_body["room_id"]) - self.assertEquals("join", channel.json_body["membership"]) - - # Room state is easier to assert on if we unpack it into a dict - state = {} - for event in channel.json_body["state"]: - if "state_key" not in event: - continue - t = event["type"] - if t not in state: - state[t] = [] - state[t].append(event) - - self.assertTrue("m.room.create" in state) - - self.assertTrue("messages" in channel.json_body) - self.assertTrue("chunk" in channel.json_body["messages"]) - self.assertTrue("end" in channel.json_body["messages"]) - - self.assertTrue("presence" in channel.json_body) - - presence_by_user = { - e["content"]["user_id"]: e for e in channel.json_body["presence"] - } - self.assertTrue(self.user_id in presence_by_user) - self.assertEquals("m.presence", presence_by_user[self.user_id]["type"]) - - -class RoomMessageListTestCase(RoomBase): - """Tests /rooms/$room_id/messages REST events.""" - - user_id = "@sid1:red" - - def prepare(self, reactor, clock, hs): - self.room_id = self.helper.create_room_as(self.user_id) - - def test_topo_token_is_accepted(self): - token = "t1-0_0_0_0_0_0_0_0_0" - channel = self.make_request( - "GET", "/rooms/%s/messages?access_token=x&from=%s" % (self.room_id, token) - ) - self.assertEquals(200, channel.code) - self.assertTrue("start" in channel.json_body) - self.assertEquals(token, channel.json_body["start"]) - self.assertTrue("chunk" in channel.json_body) - self.assertTrue("end" in channel.json_body) - - def test_stream_token_is_accepted_for_fwd_pagianation(self): - token = "s0_0_0_0_0_0_0_0_0" - channel = self.make_request( - "GET", "/rooms/%s/messages?access_token=x&from=%s" % (self.room_id, token) - ) - self.assertEquals(200, channel.code) - self.assertTrue("start" in channel.json_body) - self.assertEquals(token, channel.json_body["start"]) - self.assertTrue("chunk" in channel.json_body) - self.assertTrue("end" in channel.json_body) - - def test_room_messages_purge(self): - store = self.hs.get_datastore() - pagination_handler = self.hs.get_pagination_handler() - - # Send a first message in the room, which will be removed by the purge. - first_event_id = self.helper.send(self.room_id, "message 1")["event_id"] - first_token = self.get_success( - store.get_topological_token_for_event(first_event_id) - ) - first_token_str = self.get_success(first_token.to_string(store)) - - # Send a second message in the room, which won't be removed, and which we'll - # use as the marker to purge events before. - second_event_id = self.helper.send(self.room_id, "message 2")["event_id"] - second_token = self.get_success( - store.get_topological_token_for_event(second_event_id) - ) - second_token_str = self.get_success(second_token.to_string(store)) - - # Send a third event in the room to ensure we don't fall under any edge case - # due to our marker being the latest forward extremity in the room. - self.helper.send(self.room_id, "message 3") - - # Check that we get the first and second message when querying /messages. - channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" - % ( - self.room_id, - second_token_str, - json.dumps({"types": [EventTypes.Message]}), - ), - ) - self.assertEqual(channel.code, 200, channel.json_body) - - chunk = channel.json_body["chunk"] - self.assertEqual(len(chunk), 2, [event["content"] for event in chunk]) - - # Purge every event before the second event. - purge_id = random_string(16) - pagination_handler._purges_by_id[purge_id] = PurgeStatus() - self.get_success( - pagination_handler._purge_history( - purge_id=purge_id, - room_id=self.room_id, - token=second_token_str, - delete_local_events=True, - ) - ) - - # Check that we only get the second message through /message now that the first - # has been purged. - channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" - % ( - self.room_id, - second_token_str, - json.dumps({"types": [EventTypes.Message]}), - ), - ) - self.assertEqual(channel.code, 200, channel.json_body) - - chunk = channel.json_body["chunk"] - self.assertEqual(len(chunk), 1, [event["content"] for event in chunk]) - - # Check that we get no event, but also no error, when querying /messages with - # the token that was pointing at the first event, because we don't have it - # anymore. - channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" - % ( - self.room_id, - first_token_str, - json.dumps({"types": [EventTypes.Message]}), - ), - ) - self.assertEqual(channel.code, 200, channel.json_body) - - chunk = channel.json_body["chunk"] - self.assertEqual(len(chunk), 0, [event["content"] for event in chunk]) - - -class RoomSearchTestCase(unittest.HomeserverTestCase): - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - ] - user_id = True - hijack_auth = False - - def prepare(self, reactor, clock, hs): - - # Register the user who does the searching - self.user_id = self.register_user("user", "pass") - self.access_token = self.login("user", "pass") - - # Register the user who sends the message - self.other_user_id = self.register_user("otheruser", "pass") - self.other_access_token = self.login("otheruser", "pass") - - # Create a room - self.room = self.helper.create_room_as(self.user_id, tok=self.access_token) - - # Invite the other person - self.helper.invite( - room=self.room, - src=self.user_id, - tok=self.access_token, - targ=self.other_user_id, - ) - - # The other user joins - self.helper.join( - room=self.room, user=self.other_user_id, tok=self.other_access_token - ) - - def test_finds_message(self): - """ - The search functionality will search for content in messages if asked to - do so. - """ - # The other user sends some messages - self.helper.send(self.room, body="Hi!", tok=self.other_access_token) - self.helper.send(self.room, body="There!", tok=self.other_access_token) - - channel = self.make_request( - "POST", - "/search?access_token=%s" % (self.access_token,), - { - "search_categories": { - "room_events": {"keys": ["content.body"], "search_term": "Hi"} - } - }, - ) - - # Check we get the results we expect -- one search result, of the sent - # messages - self.assertEqual(channel.code, 200) - results = channel.json_body["search_categories"]["room_events"] - self.assertEqual(results["count"], 1) - self.assertEqual(results["results"][0]["result"]["content"]["body"], "Hi!") - - # No context was requested, so we should get none. - self.assertEqual(results["results"][0]["context"], {}) - - def test_include_context(self): - """ - When event_context includes include_profile, profile information will be - included in the search response. - """ - # The other user sends some messages - self.helper.send(self.room, body="Hi!", tok=self.other_access_token) - self.helper.send(self.room, body="There!", tok=self.other_access_token) - - channel = self.make_request( - "POST", - "/search?access_token=%s" % (self.access_token,), - { - "search_categories": { - "room_events": { - "keys": ["content.body"], - "search_term": "Hi", - "event_context": {"include_profile": True}, - } - } - }, - ) - - # Check we get the results we expect -- one search result, of the sent - # messages - self.assertEqual(channel.code, 200) - results = channel.json_body["search_categories"]["room_events"] - self.assertEqual(results["count"], 1) - self.assertEqual(results["results"][0]["result"]["content"]["body"], "Hi!") - - # We should get context info, like the two users, and the display names. - context = results["results"][0]["context"] - self.assertEqual(len(context["profile_info"].keys()), 2) - self.assertEqual( - context["profile_info"][self.other_user_id]["displayname"], "otheruser" - ) - - -class PublicRoomsRestrictedTestCase(unittest.HomeserverTestCase): - - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - - self.url = b"/_matrix/client/r0/publicRooms" - - config = self.default_config() - config["allow_public_rooms_without_auth"] = False - self.hs = self.setup_test_homeserver(config=config) - - return self.hs - - def test_restricted_no_auth(self): - channel = self.make_request("GET", self.url) - self.assertEqual(channel.code, 401, channel.result) - - def test_restricted_auth(self): - self.register_user("user", "pass") - tok = self.login("user", "pass") - - channel = self.make_request("GET", self.url, access_token=tok) - self.assertEqual(channel.code, 200, channel.result) - - -class PublicRoomsTestRemoteSearchFallbackTestCase(unittest.HomeserverTestCase): - """Test that we correctly fallback to local filtering if a remote server - doesn't support search. - """ - - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - return self.setup_test_homeserver(federation_client=Mock()) - - def prepare(self, reactor, clock, hs): - self.register_user("user", "pass") - self.token = self.login("user", "pass") - - self.federation_client = hs.get_federation_client() - - def test_simple(self): - "Simple test for searching rooms over federation" - self.federation_client.get_public_rooms.side_effect = ( - lambda *a, **k: defer.succeed({}) - ) - - search_filter = {"generic_search_term": "foobar"} - - channel = self.make_request( - "POST", - b"/_matrix/client/r0/publicRooms?server=testserv", - content={"filter": search_filter}, - access_token=self.token, - ) - self.assertEqual(channel.code, 200, channel.result) - - self.federation_client.get_public_rooms.assert_called_once_with( - "testserv", - limit=100, - since_token=None, - search_filter=search_filter, - include_all_networks=False, - third_party_instance_id=None, - ) - - def test_fallback(self): - "Test that searching public rooms over federation falls back if it gets a 404" - - # The `get_public_rooms` should be called again if the first call fails - # with a 404, when using search filters. - self.federation_client.get_public_rooms.side_effect = ( - HttpResponseException(404, "Not Found", b""), - defer.succeed({}), - ) - - search_filter = {"generic_search_term": "foobar"} - - channel = self.make_request( - "POST", - b"/_matrix/client/r0/publicRooms?server=testserv", - content={"filter": search_filter}, - access_token=self.token, - ) - self.assertEqual(channel.code, 200, channel.result) - - self.federation_client.get_public_rooms.assert_has_calls( - [ - call( - "testserv", - limit=100, - since_token=None, - search_filter=search_filter, - include_all_networks=False, - third_party_instance_id=None, - ), - call( - "testserv", - limit=None, - since_token=None, - search_filter=None, - include_all_networks=False, - third_party_instance_id=None, - ), - ] - ) - - -class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): - - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - profile.register_servlets, - ] - - def make_homeserver(self, reactor, clock): - config = self.default_config() - config["allow_per_room_profiles"] = False - self.hs = self.setup_test_homeserver(config=config) - - return self.hs - - def prepare(self, reactor, clock, homeserver): - self.user_id = self.register_user("test", "test") - self.tok = self.login("test", "test") - - # Set a profile for the test user - self.displayname = "test user" - data = {"displayname": self.displayname} - request_data = json.dumps(data) - channel = self.make_request( - "PUT", - "/_matrix/client/r0/profile/%s/displayname" % (self.user_id,), - request_data, - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) - - def test_per_room_profile_forbidden(self): - data = {"membership": "join", "displayname": "other test user"} - request_data = json.dumps(data) - channel = self.make_request( - "PUT", - "/_matrix/client/r0/rooms/%s/state/m.room.member/%s" - % (self.room_id, self.user_id), - request_data, - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - event_id = channel.json_body["event_id"] - - channel = self.make_request( - "GET", - "/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, event_id), - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - res_displayname = channel.json_body["content"]["displayname"] - self.assertEqual(res_displayname, self.displayname, channel.result) - - -class RoomMembershipReasonTestCase(unittest.HomeserverTestCase): - """Tests that clients can add a "reason" field to membership events and - that they get correctly added to the generated events and propagated. - """ - - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - ] - - def prepare(self, reactor, clock, homeserver): - self.creator = self.register_user("creator", "test") - self.creator_tok = self.login("creator", "test") - - self.second_user_id = self.register_user("second", "test") - self.second_tok = self.login("second", "test") - - self.room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok) - - def test_join_reason(self): - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/join", - content={"reason": reason}, - access_token=self.second_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def test_leave_reason(self): - self.helper.join(self.room_id, user=self.second_user_id, tok=self.second_tok) - - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/leave", - content={"reason": reason}, - access_token=self.second_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def test_kick_reason(self): - self.helper.join(self.room_id, user=self.second_user_id, tok=self.second_tok) - - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/kick", - content={"reason": reason, "user_id": self.second_user_id}, - access_token=self.second_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def test_ban_reason(self): - self.helper.join(self.room_id, user=self.second_user_id, tok=self.second_tok) - - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/ban", - content={"reason": reason, "user_id": self.second_user_id}, - access_token=self.creator_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def test_unban_reason(self): - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/unban", - content={"reason": reason, "user_id": self.second_user_id}, - access_token=self.creator_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def test_invite_reason(self): - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/invite", - content={"reason": reason, "user_id": self.second_user_id}, - access_token=self.creator_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def test_reject_invite_reason(self): - self.helper.invite( - self.room_id, - src=self.creator, - targ=self.second_user_id, - tok=self.creator_tok, - ) - - reason = "hello" - channel = self.make_request( - "POST", - f"/_matrix/client/r0/rooms/{self.room_id}/leave", - content={"reason": reason}, - access_token=self.second_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - self._check_for_reason(reason) - - def _check_for_reason(self, reason): - channel = self.make_request( - "GET", - "/_matrix/client/r0/rooms/{}/state/m.room.member/{}".format( - self.room_id, self.second_user_id - ), - access_token=self.creator_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - event_content = channel.json_body - - self.assertEqual(event_content.get("reason"), reason, channel.result) - - -class LabelsTestCase(unittest.HomeserverTestCase): - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - profile.register_servlets, - ] - - # Filter that should only catch messages with the label "#fun". - FILTER_LABELS = { - "types": [EventTypes.Message], - "org.matrix.labels": ["#fun"], - } - # Filter that should only catch messages without the label "#fun". - FILTER_NOT_LABELS = { - "types": [EventTypes.Message], - "org.matrix.not_labels": ["#fun"], - } - # Filter that should only catch messages with the label "#work" but without the label - # "#notfun". - FILTER_LABELS_NOT_LABELS = { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - - def prepare(self, reactor, clock, homeserver): - self.user_id = self.register_user("test", "test") - self.tok = self.login("test", "test") - self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) - - def test_context_filter_labels(self): - """Test that we can filter by a label on a /context request.""" - event_id = self._send_labelled_messages_in_room() - - channel = self.make_request( - "GET", - "/rooms/%s/context/%s?filter=%s" - % (self.room_id, event_id, json.dumps(self.FILTER_LABELS)), - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - events_before = channel.json_body["events_before"] - - self.assertEqual( - len(events_before), 1, [event["content"] for event in events_before] - ) - self.assertEqual( - events_before[0]["content"]["body"], "with right label", events_before[0] - ) - - events_after = channel.json_body["events_before"] - - self.assertEqual( - len(events_after), 1, [event["content"] for event in events_after] - ) - self.assertEqual( - events_after[0]["content"]["body"], "with right label", events_after[0] - ) - - def test_context_filter_not_labels(self): - """Test that we can filter by the absence of a label on a /context request.""" - event_id = self._send_labelled_messages_in_room() - - channel = self.make_request( - "GET", - "/rooms/%s/context/%s?filter=%s" - % (self.room_id, event_id, json.dumps(self.FILTER_NOT_LABELS)), - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - events_before = channel.json_body["events_before"] - - self.assertEqual( - len(events_before), 1, [event["content"] for event in events_before] - ) - self.assertEqual( - events_before[0]["content"]["body"], "without label", events_before[0] - ) - - events_after = channel.json_body["events_after"] - - self.assertEqual( - len(events_after), 2, [event["content"] for event in events_after] - ) - self.assertEqual( - events_after[0]["content"]["body"], "with wrong label", events_after[0] - ) - self.assertEqual( - events_after[1]["content"]["body"], "with two wrong labels", events_after[1] - ) - - def test_context_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label on a - /context request. - """ - event_id = self._send_labelled_messages_in_room() - - channel = self.make_request( - "GET", - "/rooms/%s/context/%s?filter=%s" - % (self.room_id, event_id, json.dumps(self.FILTER_LABELS_NOT_LABELS)), - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - events_before = channel.json_body["events_before"] - - self.assertEqual( - len(events_before), 0, [event["content"] for event in events_before] - ) - - events_after = channel.json_body["events_after"] - - self.assertEqual( - len(events_after), 1, [event["content"] for event in events_after] - ) - self.assertEqual( - events_after[0]["content"]["body"], "with wrong label", events_after[0] - ) - - def test_messages_filter_labels(self): - """Test that we can filter by a label on a /messages request.""" - self._send_labelled_messages_in_room() - - token = "s0_0_0_0_0_0_0_0_0" - channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS)), - ) - - events = channel.json_body["chunk"] - - self.assertEqual(len(events), 2, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) - self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) - - def test_messages_filter_not_labels(self): - """Test that we can filter by the absence of a label on a /messages request.""" - self._send_labelled_messages_in_room() - - token = "s0_0_0_0_0_0_0_0_0" - channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, json.dumps(self.FILTER_NOT_LABELS)), - ) - - events = channel.json_body["chunk"] - - self.assertEqual(len(events), 4, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "without label", events[0]) - self.assertEqual(events[1]["content"]["body"], "without label", events[1]) - self.assertEqual(events[2]["content"]["body"], "with wrong label", events[2]) - self.assertEqual( - events[3]["content"]["body"], "with two wrong labels", events[3] - ) - - def test_messages_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label on a - /messages request. - """ - self._send_labelled_messages_in_room() - - token = "s0_0_0_0_0_0_0_0_0" - channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % ( - self.room_id, - self.tok, - token, - json.dumps(self.FILTER_LABELS_NOT_LABELS), - ), - ) - - events = channel.json_body["chunk"] - - self.assertEqual(len(events), 1, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) - - def test_search_filter_labels(self): - """Test that we can filter by a label on a /search request.""" - request_data = json.dumps( - { - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_LABELS, - } - } - } - ) - - self._send_labelled_messages_in_room() - - channel = self.make_request( - "POST", "/search?access_token=%s" % self.tok, request_data - ) - - results = channel.json_body["search_categories"]["room_events"]["results"] - - self.assertEqual( - len(results), - 2, - [result["result"]["content"] for result in results], - ) - self.assertEqual( - results[0]["result"]["content"]["body"], - "with right label", - results[0]["result"]["content"]["body"], - ) - self.assertEqual( - results[1]["result"]["content"]["body"], - "with right label", - results[1]["result"]["content"]["body"], - ) - - def test_search_filter_not_labels(self): - """Test that we can filter by the absence of a label on a /search request.""" - request_data = json.dumps( - { - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_NOT_LABELS, - } - } - } - ) - - self._send_labelled_messages_in_room() - - channel = self.make_request( - "POST", "/search?access_token=%s" % self.tok, request_data - ) - - results = channel.json_body["search_categories"]["room_events"]["results"] - - self.assertEqual( - len(results), - 4, - [result["result"]["content"] for result in results], - ) - self.assertEqual( - results[0]["result"]["content"]["body"], - "without label", - results[0]["result"]["content"]["body"], - ) - self.assertEqual( - results[1]["result"]["content"]["body"], - "without label", - results[1]["result"]["content"]["body"], - ) - self.assertEqual( - results[2]["result"]["content"]["body"], - "with wrong label", - results[2]["result"]["content"]["body"], - ) - self.assertEqual( - results[3]["result"]["content"]["body"], - "with two wrong labels", - results[3]["result"]["content"]["body"], - ) - - def test_search_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label on a - /search request. - """ - request_data = json.dumps( - { - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_LABELS_NOT_LABELS, - } - } - } - ) - - self._send_labelled_messages_in_room() - - channel = self.make_request( - "POST", "/search?access_token=%s" % self.tok, request_data - ) - - results = channel.json_body["search_categories"]["room_events"]["results"] - - self.assertEqual( - len(results), - 1, - [result["result"]["content"] for result in results], - ) - self.assertEqual( - results[0]["result"]["content"]["body"], - "with wrong label", - results[0]["result"]["content"]["body"], - ) - - def _send_labelled_messages_in_room(self): - """Sends several messages to a room with different labels (or without any) to test - filtering by label. - Returns: - The ID of the event to use if we're testing filtering on /context. - """ - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - tok=self.tok, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={"msgtype": "m.text", "body": "without label"}, - tok=self.tok, - ) - - res = self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={"msgtype": "m.text", "body": "without label"}, - tok=self.tok, - ) - # Return this event's ID when we test filtering in /context requests. - event_id = res["event_id"] - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with wrong label", - EventContentFields.LABELS: ["#work"], - }, - tok=self.tok, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with two wrong labels", - EventContentFields.LABELS: ["#work", "#notfun"], - }, - tok=self.tok, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - tok=self.tok, - ) - - return event_id - - -class ContextTestCase(unittest.HomeserverTestCase): - - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - room.register_servlets, - login.register_servlets, - account.register_servlets, - ] - - def prepare(self, reactor, clock, homeserver): - self.user_id = self.register_user("user", "password") - self.tok = self.login("user", "password") - self.room_id = self.helper.create_room_as( - self.user_id, tok=self.tok, is_public=False - ) - - self.other_user_id = self.register_user("user2", "password") - self.other_tok = self.login("user2", "password") - - self.helper.invite(self.room_id, self.user_id, self.other_user_id, tok=self.tok) - self.helper.join(self.room_id, self.other_user_id, tok=self.other_tok) - - def test_erased_sender(self): - """Test that an erasure request results in the requester's events being hidden - from any new member of the room. - """ - - # Send a bunch of events in the room. - - self.helper.send(self.room_id, "message 1", tok=self.tok) - self.helper.send(self.room_id, "message 2", tok=self.tok) - event_id = self.helper.send(self.room_id, "message 3", tok=self.tok)["event_id"] - self.helper.send(self.room_id, "message 4", tok=self.tok) - self.helper.send(self.room_id, "message 5", tok=self.tok) - - # Check that we can still see the messages before the erasure request. - - channel = self.make_request( - "GET", - '/rooms/%s/context/%s?filter={"types":["m.room.message"]}' - % (self.room_id, event_id), - access_token=self.tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - events_before = channel.json_body["events_before"] - - self.assertEqual(len(events_before), 2, events_before) - self.assertEqual( - events_before[0].get("content", {}).get("body"), - "message 2", - events_before[0], - ) - self.assertEqual( - events_before[1].get("content", {}).get("body"), - "message 1", - events_before[1], - ) - - self.assertEqual( - channel.json_body["event"].get("content", {}).get("body"), - "message 3", - channel.json_body["event"], - ) - - events_after = channel.json_body["events_after"] - - self.assertEqual(len(events_after), 2, events_after) - self.assertEqual( - events_after[0].get("content", {}).get("body"), - "message 4", - events_after[0], - ) - self.assertEqual( - events_after[1].get("content", {}).get("body"), - "message 5", - events_after[1], - ) - - # Deactivate the first account and erase the user's data. - - deactivate_account_handler = self.hs.get_deactivate_account_handler() - self.get_success( - deactivate_account_handler.deactivate_account( - self.user_id, True, create_requester(self.user_id) - ) - ) - - # Invite another user in the room. This is needed because messages will be - # pruned only if the user wasn't a member of the room when the messages were - # sent. - - invited_user_id = self.register_user("user3", "password") - invited_tok = self.login("user3", "password") - - self.helper.invite( - self.room_id, self.other_user_id, invited_user_id, tok=self.other_tok - ) - self.helper.join(self.room_id, invited_user_id, tok=invited_tok) - - # Check that a user that joined the room after the erasure request can't see - # the messages anymore. - - channel = self.make_request( - "GET", - '/rooms/%s/context/%s?filter={"types":["m.room.message"]}' - % (self.room_id, event_id), - access_token=invited_tok, - ) - self.assertEqual(channel.code, 200, channel.result) - - events_before = channel.json_body["events_before"] - - self.assertEqual(len(events_before), 2, events_before) - self.assertDictEqual(events_before[0].get("content"), {}, events_before[0]) - self.assertDictEqual(events_before[1].get("content"), {}, events_before[1]) - - self.assertDictEqual( - channel.json_body["event"].get("content"), {}, channel.json_body["event"] - ) - - events_after = channel.json_body["events_after"] - - self.assertEqual(len(events_after), 2, events_after) - self.assertDictEqual(events_after[0].get("content"), {}, events_after[0]) - self.assertEqual(events_after[1].get("content"), {}, events_after[1]) - - -class RoomAliasListTestCase(unittest.HomeserverTestCase): - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - directory.register_servlets, - login.register_servlets, - room.register_servlets, - ] - - def prepare(self, reactor, clock, homeserver): - self.room_owner = self.register_user("room_owner", "test") - self.room_owner_tok = self.login("room_owner", "test") - - self.room_id = self.helper.create_room_as( - self.room_owner, tok=self.room_owner_tok - ) - - def test_no_aliases(self): - res = self._get_aliases(self.room_owner_tok) - self.assertEqual(res["aliases"], []) - - def test_not_in_room(self): - self.register_user("user", "test") - user_tok = self.login("user", "test") - res = self._get_aliases(user_tok, expected_code=403) - self.assertEqual(res["errcode"], "M_FORBIDDEN") - - def test_admin_user(self): - alias1 = self._random_alias() - self._set_alias_via_directory(alias1) - - self.register_user("user", "test", admin=True) - user_tok = self.login("user", "test") - - res = self._get_aliases(user_tok) - self.assertEqual(res["aliases"], [alias1]) - - def test_with_aliases(self): - alias1 = self._random_alias() - alias2 = self._random_alias() - - self._set_alias_via_directory(alias1) - self._set_alias_via_directory(alias2) - - res = self._get_aliases(self.room_owner_tok) - self.assertEqual(set(res["aliases"]), {alias1, alias2}) - - def test_peekable_room(self): - alias1 = self._random_alias() - self._set_alias_via_directory(alias1) - - self.helper.send_state( - self.room_id, - EventTypes.RoomHistoryVisibility, - body={"history_visibility": "world_readable"}, - tok=self.room_owner_tok, - ) - - self.register_user("user", "test") - user_tok = self.login("user", "test") - - res = self._get_aliases(user_tok) - self.assertEqual(res["aliases"], [alias1]) - - def _get_aliases(self, access_token: str, expected_code: int = 200) -> JsonDict: - """Calls the endpoint under test. returns the json response object.""" - channel = self.make_request( - "GET", - "/_matrix/client/r0/rooms/%s/aliases" % (self.room_id,), - access_token=access_token, - ) - self.assertEqual(channel.code, expected_code, channel.result) - res = channel.json_body - self.assertIsInstance(res, dict) - if expected_code == 200: - self.assertIsInstance(res["aliases"], list) - return res - - def _random_alias(self) -> str: - return RoomAlias(random_string(5), self.hs.hostname).to_string() - - def _set_alias_via_directory(self, alias: str, expected_code: int = 200): - url = "/_matrix/client/r0/directory/room/" + alias - data = {"room_id": self.room_id} - request_data = json.dumps(data) - - channel = self.make_request( - "PUT", url, request_data, access_token=self.room_owner_tok - ) - self.assertEqual(channel.code, expected_code, channel.result) - - -class RoomCanonicalAliasTestCase(unittest.HomeserverTestCase): - servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, - directory.register_servlets, - login.register_servlets, - room.register_servlets, - ] - - def prepare(self, reactor, clock, homeserver): - self.room_owner = self.register_user("room_owner", "test") - self.room_owner_tok = self.login("room_owner", "test") - - self.room_id = self.helper.create_room_as( - self.room_owner, tok=self.room_owner_tok - ) - - self.alias = "#alias:test" - self._set_alias_via_directory(self.alias) - - def _set_alias_via_directory(self, alias: str, expected_code: int = 200): - url = "/_matrix/client/r0/directory/room/" + alias - data = {"room_id": self.room_id} - request_data = json.dumps(data) - - channel = self.make_request( - "PUT", url, request_data, access_token=self.room_owner_tok - ) - self.assertEqual(channel.code, expected_code, channel.result) - - def _get_canonical_alias(self, expected_code: int = 200) -> JsonDict: - """Calls the endpoint under test. returns the json response object.""" - channel = self.make_request( - "GET", - "rooms/%s/state/m.room.canonical_alias" % (self.room_id,), - access_token=self.room_owner_tok, - ) - self.assertEqual(channel.code, expected_code, channel.result) - res = channel.json_body - self.assertIsInstance(res, dict) - return res - - def _set_canonical_alias(self, content: str, expected_code: int = 200) -> JsonDict: - """Calls the endpoint under test. returns the json response object.""" - channel = self.make_request( - "PUT", - "rooms/%s/state/m.room.canonical_alias" % (self.room_id,), - json.dumps(content), - access_token=self.room_owner_tok, - ) - self.assertEqual(channel.code, expected_code, channel.result) - res = channel.json_body - self.assertIsInstance(res, dict) - return res - - def test_canonical_alias(self): - """Test a basic alias message.""" - # There is no canonical alias to start with. - self._get_canonical_alias(expected_code=404) - - # Create an alias. - self._set_canonical_alias({"alias": self.alias}) - - # Canonical alias now exists! - res = self._get_canonical_alias() - self.assertEqual(res, {"alias": self.alias}) - - # Now remove the alias. - self._set_canonical_alias({}) - - # There is an alias event, but it is empty. - res = self._get_canonical_alias() - self.assertEqual(res, {}) - - def test_alt_aliases(self): - """Test a canonical alias message with alt_aliases.""" - # Create an alias. - self._set_canonical_alias({"alt_aliases": [self.alias]}) - - # Canonical alias now exists! - res = self._get_canonical_alias() - self.assertEqual(res, {"alt_aliases": [self.alias]}) - - # Now remove the alt_aliases. - self._set_canonical_alias({}) - - # There is an alias event, but it is empty. - res = self._get_canonical_alias() - self.assertEqual(res, {}) - - def test_alias_alt_aliases(self): - """Test a canonical alias message with an alias and alt_aliases.""" - # Create an alias. - self._set_canonical_alias({"alias": self.alias, "alt_aliases": [self.alias]}) - - # Canonical alias now exists! - res = self._get_canonical_alias() - self.assertEqual(res, {"alias": self.alias, "alt_aliases": [self.alias]}) - - # Now remove the alias and alt_aliases. - self._set_canonical_alias({}) - - # There is an alias event, but it is empty. - res = self._get_canonical_alias() - self.assertEqual(res, {}) - - def test_partial_modify(self): - """Test removing only the alt_aliases.""" - # Create an alias. - self._set_canonical_alias({"alias": self.alias, "alt_aliases": [self.alias]}) - - # Canonical alias now exists! - res = self._get_canonical_alias() - self.assertEqual(res, {"alias": self.alias, "alt_aliases": [self.alias]}) - - # Now remove the alt_aliases. - self._set_canonical_alias({"alias": self.alias}) - - # There is an alias event, but it is empty. - res = self._get_canonical_alias() - self.assertEqual(res, {"alias": self.alias}) - - def test_add_alias(self): - """Test removing only the alt_aliases.""" - # Create an additional alias. - second_alias = "#second:test" - self._set_alias_via_directory(second_alias) - - # Add the canonical alias. - self._set_canonical_alias({"alias": self.alias, "alt_aliases": [self.alias]}) - - # Then add the second alias. - self._set_canonical_alias( - {"alias": self.alias, "alt_aliases": [self.alias, second_alias]} - ) - - # Canonical alias now exists! - res = self._get_canonical_alias() - self.assertEqual( - res, {"alias": self.alias, "alt_aliases": [self.alias, second_alias]} - ) - - def test_bad_data(self): - """Invalid data for alt_aliases should cause errors.""" - self._set_canonical_alias({"alt_aliases": "@bad:test"}, expected_code=400) - self._set_canonical_alias({"alt_aliases": None}, expected_code=400) - self._set_canonical_alias({"alt_aliases": 0}, expected_code=400) - self._set_canonical_alias({"alt_aliases": 1}, expected_code=400) - self._set_canonical_alias({"alt_aliases": False}, expected_code=400) - self._set_canonical_alias({"alt_aliases": True}, expected_code=400) - self._set_canonical_alias({"alt_aliases": {}}, expected_code=400) - - def test_bad_alias(self): - """An alias which does not point to the room raises a SynapseError.""" - self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400) - self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400) diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py deleted file mode 100644 index b54b004733..0000000000 --- a/tests/rest/client/v1/test_typing.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector -# -# 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. - -"""Tests REST events for /rooms paths.""" - -from unittest.mock import Mock - -from synapse.rest.client import room -from synapse.types import UserID - -from tests import unittest - -PATH_PREFIX = "/_matrix/client/api/v1" - - -class RoomTypingTestCase(unittest.HomeserverTestCase): - """Tests /rooms/$room_id/typing/$user_id REST API.""" - - user_id = "@sid:red" - - user = UserID.from_string(user_id) - servlets = [room.register_servlets] - - def make_homeserver(self, reactor, clock): - - hs = self.setup_test_homeserver( - "red", - federation_http_client=None, - federation_client=Mock(), - ) - - self.event_source = hs.get_event_sources().sources["typing"] - - hs.get_federation_handler = Mock() - - async def get_user_by_access_token(token=None, allow_guest=False): - return { - "user": UserID.from_string(self.auth_user_id), - "token_id": 1, - "is_guest": False, - } - - hs.get_auth().get_user_by_access_token = get_user_by_access_token - - async def _insert_client_ip(*args, **kwargs): - return None - - hs.get_datastore().insert_client_ip = _insert_client_ip - - return hs - - def prepare(self, reactor, clock, hs): - self.room_id = self.helper.create_room_as(self.user_id) - # Need another user to make notifications actually work - self.helper.join(self.room_id, user="@jim:red") - - def test_set_typing(self): - channel = self.make_request( - "PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - b'{"typing": true, "timeout": 30000}', - ) - self.assertEquals(200, channel.code) - - self.assertEquals(self.event_source.get_current_key(), 1) - events = self.get_success( - self.event_source.get_new_events(from_key=0, room_ids=[self.room_id]) - ) - self.assertEquals( - events[0], - [ - { - "type": "m.typing", - "room_id": self.room_id, - "content": {"user_ids": [self.user_id]}, - } - ], - ) - - def test_set_not_typing(self): - channel = self.make_request( - "PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - b'{"typing": false}', - ) - self.assertEquals(200, channel.code) - - def test_typing_timeout(self): - channel = self.make_request( - "PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - b'{"typing": true, "timeout": 30000}', - ) - self.assertEquals(200, channel.code) - - self.assertEquals(self.event_source.get_current_key(), 1) - - self.reactor.advance(36) - - self.assertEquals(self.event_source.get_current_key(), 2) - - channel = self.make_request( - "PUT", - "/rooms/%s/typing/%s" % (self.room_id, self.user_id), - b'{"typing": true, "timeout": 30000}', - ) - self.assertEquals(200, channel.code) - - self.assertEquals(self.event_source.get_current_key(), 3) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py deleted file mode 100644 index 954ad1a1fd..0000000000 --- a/tests/rest/client/v1/utils.py +++ /dev/null @@ -1,654 +0,0 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2017 Vector Creations Ltd -# Copyright 2018-2019 New Vector Ltd -# Copyright 2019-2021 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import re -import time -import urllib.parse -from typing import Any, Dict, Iterable, Mapping, MutableMapping, Optional, Tuple, Union -from unittest.mock import patch - -import attr - -from twisted.web.resource import Resource -from twisted.web.server import Site - -from synapse.api.constants import Membership -from synapse.types import JsonDict - -from tests.server import FakeChannel, FakeSite, make_request -from tests.test_utils import FakeResponse -from tests.test_utils.html_parsers import TestHtmlParser - - -@attr.s -class RestHelper: - """Contains extra helper functions to quickly and clearly perform a given - REST action, which isn't the focus of the test. - """ - - hs = attr.ib() - site = attr.ib(type=Site) - auth_user_id = attr.ib() - - def create_room_as( - self, - room_creator: Optional[str] = None, - is_public: bool = True, - room_version: Optional[str] = None, - tok: Optional[str] = None, - expect_code: int = 200, - extra_content: Optional[Dict] = None, - custom_headers: Optional[ - Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] - ] = None, - ) -> str: - """ - Create a room. - - Args: - room_creator: The user ID to create the room with. - is_public: If True, the `visibility` parameter will be set to the - default (public). Otherwise, the `visibility` parameter will be set - to "private". - room_version: The room version to create the room as. Defaults to Synapse's - default room version. - tok: The access token to use in the request. - expect_code: The expected HTTP response code. - - Returns: - The ID of the newly created room. - """ - temp_id = self.auth_user_id - self.auth_user_id = room_creator - path = "/_matrix/client/r0/createRoom" - content = extra_content or {} - if not is_public: - content["visibility"] = "private" - if room_version: - content["room_version"] = room_version - if tok: - path = path + "?access_token=%s" % tok - - channel = make_request( - self.hs.get_reactor(), - self.site, - "POST", - path, - json.dumps(content).encode("utf8"), - custom_headers=custom_headers, - ) - - assert channel.result["code"] == b"%d" % expect_code, channel.result - self.auth_user_id = temp_id - - if expect_code == 200: - return channel.json_body["room_id"] - - def invite(self, room=None, src=None, targ=None, expect_code=200, tok=None): - self.change_membership( - room=room, - src=src, - targ=targ, - tok=tok, - membership=Membership.INVITE, - expect_code=expect_code, - ) - - def join(self, room=None, user=None, expect_code=200, tok=None): - self.change_membership( - room=room, - src=user, - targ=user, - tok=tok, - membership=Membership.JOIN, - expect_code=expect_code, - ) - - def leave(self, room=None, user=None, expect_code=200, tok=None): - self.change_membership( - room=room, - src=user, - targ=user, - tok=tok, - membership=Membership.LEAVE, - expect_code=expect_code, - ) - - def change_membership( - self, - room: str, - src: str, - targ: str, - membership: str, - extra_data: Optional[dict] = None, - tok: Optional[str] = None, - expect_code: int = 200, - ) -> None: - """ - Send a membership state event into a room. - - Args: - room: The ID of the room to send to - src: The mxid of the event sender - targ: The mxid of the event's target. The state key - membership: The type of membership event - extra_data: Extra information to include in the content of the event - tok: The user access token to use - expect_code: The expected HTTP response code - """ - temp_id = self.auth_user_id - self.auth_user_id = src - - path = "/_matrix/client/r0/rooms/%s/state/m.room.member/%s" % (room, targ) - if tok: - path = path + "?access_token=%s" % tok - - data = {"membership": membership} - data.update(extra_data or {}) - - channel = make_request( - self.hs.get_reactor(), - self.site, - "PUT", - path, - json.dumps(data).encode("utf8"), - ) - - assert ( - int(channel.result["code"]) == expect_code - ), "Expected: %d, got: %d, resp: %r" % ( - expect_code, - int(channel.result["code"]), - channel.result["body"], - ) - - self.auth_user_id = temp_id - - def send( - self, - room_id, - body=None, - txn_id=None, - tok=None, - expect_code=200, - custom_headers: Optional[ - Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] - ] = None, - ): - if body is None: - body = "body_text_here" - - content = {"msgtype": "m.text", "body": body} - - return self.send_event( - room_id, - "m.room.message", - content, - txn_id, - tok, - expect_code, - custom_headers=custom_headers, - ) - - def send_event( - self, - room_id, - type, - content: Optional[dict] = None, - txn_id=None, - tok=None, - expect_code=200, - custom_headers: Optional[ - Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] - ] = None, - ): - if txn_id is None: - txn_id = "m%s" % (str(time.time())) - - path = "/_matrix/client/r0/rooms/%s/send/%s/%s" % (room_id, type, txn_id) - if tok: - path = path + "?access_token=%s" % tok - - channel = make_request( - self.hs.get_reactor(), - self.site, - "PUT", - path, - json.dumps(content or {}).encode("utf8"), - custom_headers=custom_headers, - ) - - assert ( - int(channel.result["code"]) == expect_code - ), "Expected: %d, got: %d, resp: %r" % ( - expect_code, - int(channel.result["code"]), - channel.result["body"], - ) - - return channel.json_body - - def _read_write_state( - self, - room_id: str, - event_type: str, - body: Optional[Dict[str, Any]], - tok: str, - expect_code: int = 200, - state_key: str = "", - method: str = "GET", - ) -> Dict: - """Read or write some state from a given room - - Args: - room_id: - event_type: The type of state event - body: Body that is sent when making the request. The content of the state event. - If None, the request to the server will have an empty body - tok: The access token to use - expect_code: The HTTP code to expect in the response - state_key: - method: "GET" or "PUT" for reading or writing state, respectively - - Returns: - The response body from the server - - Raises: - AssertionError: if expect_code doesn't match the HTTP code we received - """ - path = "/_matrix/client/r0/rooms/%s/state/%s/%s" % ( - room_id, - event_type, - state_key, - ) - if tok: - path = path + "?access_token=%s" % tok - - # Set request body if provided - content = b"" - if body is not None: - content = json.dumps(body).encode("utf8") - - channel = make_request(self.hs.get_reactor(), self.site, method, path, content) - - assert ( - int(channel.result["code"]) == expect_code - ), "Expected: %d, got: %d, resp: %r" % ( - expect_code, - int(channel.result["code"]), - channel.result["body"], - ) - - return channel.json_body - - def get_state( - self, - room_id: str, - event_type: str, - tok: str, - expect_code: int = 200, - state_key: str = "", - ): - """Gets some state from a room - - Args: - room_id: - event_type: The type of state event - tok: The access token to use - expect_code: The HTTP code to expect in the response - state_key: - - Returns: - The response body from the server - - Raises: - AssertionError: if expect_code doesn't match the HTTP code we received - """ - return self._read_write_state( - room_id, event_type, None, tok, expect_code, state_key, method="GET" - ) - - def send_state( - self, - room_id: str, - event_type: str, - body: Dict[str, Any], - tok: str, - expect_code: int = 200, - state_key: str = "", - ): - """Set some state in a room - - Args: - room_id: - event_type: The type of state event - body: Body that is sent when making the request. The content of the state event. - tok: The access token to use - expect_code: The HTTP code to expect in the response - state_key: - - Returns: - The response body from the server - - Raises: - AssertionError: if expect_code doesn't match the HTTP code we received - """ - return self._read_write_state( - room_id, event_type, body, tok, expect_code, state_key, method="PUT" - ) - - def upload_media( - self, - resource: Resource, - image_data: bytes, - tok: str, - filename: str = "test.png", - expect_code: int = 200, - ) -> dict: - """Upload a piece of test media to the media repo - Args: - resource: The resource that will handle the upload request - image_data: The image data to upload - tok: The user token to use during the upload - filename: The filename of the media to be uploaded - expect_code: The return code to expect from attempting to upload the media - """ - image_length = len(image_data) - path = "/_matrix/media/r0/upload?filename=%s" % (filename,) - channel = make_request( - self.hs.get_reactor(), - FakeSite(resource), - "POST", - path, - content=image_data, - access_token=tok, - custom_headers=[(b"Content-Length", str(image_length))], - ) - - assert channel.code == expect_code, "Expected: %d, got: %d, resp: %r" % ( - expect_code, - int(channel.result["code"]), - channel.result["body"], - ) - - return channel.json_body - - def login_via_oidc(self, remote_user_id: str) -> JsonDict: - """Log in (as a new user) via OIDC - - Returns the result of the final token login. - - Requires that "oidc_config" in the homeserver config be set appropriately - (TEST_OIDC_CONFIG is a suitable example) - and by implication, needs a - "public_base_url". - - Also requires the login servlet and the OIDC callback resource to be mounted at - the normal places. - """ - client_redirect_url = "https://x" - channel = self.auth_via_oidc({"sub": remote_user_id}, client_redirect_url) - - # expect a confirmation page - assert channel.code == 200, channel.result - - # fish the matrix login token out of the body of the confirmation page - m = re.search( - 'a href="%s.*loginToken=([^"]*)"' % (client_redirect_url,), - channel.text_body, - ) - assert m, channel.text_body - login_token = m.group(1) - - # finally, submit the matrix login token to the login API, which gives us our - # matrix access token and device id. - channel = make_request( - self.hs.get_reactor(), - self.site, - "POST", - "/login", - content={"type": "m.login.token", "token": login_token}, - ) - assert channel.code == 200 - return channel.json_body - - def auth_via_oidc( - self, - user_info_dict: JsonDict, - client_redirect_url: Optional[str] = None, - ui_auth_session_id: Optional[str] = None, - ) -> FakeChannel: - """Perform an OIDC authentication flow via a mock OIDC provider. - - This can be used for either login or user-interactive auth. - - Starts by making a request to the relevant synapse redirect endpoint, which is - expected to serve a 302 to the OIDC provider. We then make a request to the - OIDC callback endpoint, intercepting the HTTP requests that will get sent back - to the OIDC provider. - - Requires that "oidc_config" in the homeserver config be set appropriately - (TEST_OIDC_CONFIG is a suitable example) - and by implication, needs a - "public_base_url". - - Also requires the login servlet and the OIDC callback resource to be mounted at - the normal places. - - Args: - user_info_dict: the remote userinfo that the OIDC provider should present. - Typically this should be '{"sub": "<remote user id>"}'. - client_redirect_url: for a login flow, the client redirect URL to pass to - the login redirect endpoint - ui_auth_session_id: if set, we will perform a UI Auth flow. The session id - of the UI auth. - - Returns: - A FakeChannel containing the result of calling the OIDC callback endpoint. - Note that the response code may be a 200, 302 or 400 depending on how things - went. - """ - - cookies = {} - - # if we're doing a ui auth, hit the ui auth redirect endpoint - if ui_auth_session_id: - # can't set the client redirect url for UI Auth - assert client_redirect_url is None - oauth_uri = self.initiate_sso_ui_auth(ui_auth_session_id, cookies) - else: - # otherwise, hit the login redirect endpoint - oauth_uri = self.initiate_sso_login(client_redirect_url, cookies) - - # we now have a URI for the OIDC IdP, but we skip that and go straight - # back to synapse's OIDC callback resource. However, we do need the "state" - # param that synapse passes to the IdP via query params, as well as the cookie - # that synapse passes to the client. - - oauth_uri_path, _ = oauth_uri.split("?", 1) - assert oauth_uri_path == TEST_OIDC_AUTH_ENDPOINT, ( - "unexpected SSO URI " + oauth_uri_path - ) - return self.complete_oidc_auth(oauth_uri, cookies, user_info_dict) - - def complete_oidc_auth( - self, - oauth_uri: str, - cookies: Mapping[str, str], - user_info_dict: JsonDict, - ) -> FakeChannel: - """Mock out an OIDC authentication flow - - Assumes that an OIDC auth has been initiated by one of initiate_sso_login or - initiate_sso_ui_auth; completes the OIDC bits of the flow by making a request to - Synapse's OIDC callback endpoint, intercepting the HTTP requests that will get - sent back to the OIDC provider. - - Requires the OIDC callback resource to be mounted at the normal place. - - Args: - oauth_uri: the OIDC URI returned by synapse's redirect endpoint (ie, - from initiate_sso_login or initiate_sso_ui_auth). - cookies: the cookies set by synapse's redirect endpoint, which will be - sent back to the callback endpoint. - user_info_dict: the remote userinfo that the OIDC provider should present. - Typically this should be '{"sub": "<remote user id>"}'. - - Returns: - A FakeChannel containing the result of calling the OIDC callback endpoint. - """ - _, oauth_uri_qs = oauth_uri.split("?", 1) - params = urllib.parse.parse_qs(oauth_uri_qs) - callback_uri = "%s?%s" % ( - urllib.parse.urlparse(params["redirect_uri"][0]).path, - urllib.parse.urlencode({"state": params["state"][0], "code": "TEST_CODE"}), - ) - - # before we hit the callback uri, stub out some methods in the http client so - # that we don't have to handle full HTTPS requests. - # (expected url, json response) pairs, in the order we expect them. - expected_requests = [ - # first we get a hit to the token endpoint, which we tell to return - # a dummy OIDC access token - (TEST_OIDC_TOKEN_ENDPOINT, {"access_token": "TEST"}), - # and then one to the user_info endpoint, which returns our remote user id. - (TEST_OIDC_USERINFO_ENDPOINT, user_info_dict), - ] - - async def mock_req(method: str, uri: str, data=None, headers=None): - (expected_uri, resp_obj) = expected_requests.pop(0) - assert uri == expected_uri - resp = FakeResponse( - code=200, - phrase=b"OK", - body=json.dumps(resp_obj).encode("utf-8"), - ) - return resp - - with patch.object(self.hs.get_proxied_http_client(), "request", mock_req): - # now hit the callback URI with the right params and a made-up code - channel = make_request( - self.hs.get_reactor(), - self.site, - "GET", - callback_uri, - custom_headers=[ - ("Cookie", "%s=%s" % (k, v)) for (k, v) in cookies.items() - ], - ) - return channel - - def initiate_sso_login( - self, client_redirect_url: Optional[str], cookies: MutableMapping[str, str] - ) -> str: - """Make a request to the login-via-sso redirect endpoint, and return the target - - Assumes that exactly one SSO provider has been configured. Requires the login - servlet to be mounted. - - Args: - client_redirect_url: the client redirect URL to pass to the login redirect - endpoint - cookies: any cookies returned will be added to this dict - - Returns: - the URI that the client gets redirected to (ie, the SSO server) - """ - params = {} - if client_redirect_url: - params["redirectUrl"] = client_redirect_url - - # hit the redirect url (which should redirect back to the redirect url. This - # is the easiest way of figuring out what the Host header ought to be set to - # to keep Synapse happy. - channel = make_request( - self.hs.get_reactor(), - self.site, - "GET", - "/_matrix/client/r0/login/sso/redirect?" + urllib.parse.urlencode(params), - ) - assert channel.code == 302 - - # hit the redirect url again with the right Host header, which should now issue - # a cookie and redirect to the SSO provider. - location = channel.headers.getRawHeaders("Location")[0] - parts = urllib.parse.urlsplit(location) - channel = make_request( - self.hs.get_reactor(), - self.site, - "GET", - urllib.parse.urlunsplit(("", "") + parts[2:]), - custom_headers=[ - ("Host", parts[1]), - ], - ) - - assert channel.code == 302 - channel.extract_cookies(cookies) - return channel.headers.getRawHeaders("Location")[0] - - def initiate_sso_ui_auth( - self, ui_auth_session_id: str, cookies: MutableMapping[str, str] - ) -> str: - """Make a request to the ui-auth-via-sso endpoint, and return the target - - Assumes that exactly one SSO provider has been configured. Requires the - AuthRestServlet to be mounted. - - Args: - ui_auth_session_id: the session id of the UI auth - cookies: any cookies returned will be added to this dict - - Returns: - the URI that the client gets linked to (ie, the SSO server) - """ - sso_redirect_endpoint = ( - "/_matrix/client/r0/auth/m.login.sso/fallback/web?" - + urllib.parse.urlencode({"session": ui_auth_session_id}) - ) - # hit the redirect url (which will issue a cookie and state) - channel = make_request( - self.hs.get_reactor(), self.site, "GET", sso_redirect_endpoint - ) - # that should serve a confirmation page - assert channel.code == 200, channel.text_body - channel.extract_cookies(cookies) - - # parse the confirmation page to fish out the link. - p = TestHtmlParser() - p.feed(channel.text_body) - p.close() - assert len(p.links) == 1, "not exactly one link in confirmation page" - oauth_uri = p.links[0] - return oauth_uri - - -# an 'oidc_config' suitable for login_via_oidc. -TEST_OIDC_AUTH_ENDPOINT = "https://issuer.test/auth" -TEST_OIDC_TOKEN_ENDPOINT = "https://issuer.test/token" -TEST_OIDC_USERINFO_ENDPOINT = "https://issuer.test/userinfo" -TEST_OIDC_CONFIG = { - "enabled": True, - "discover": False, - "issuer": "https://issuer.test", - "client_id": "test-client-id", - "client_secret": "test-client-secret", - "scopes": ["profile"], - "authorization_endpoint": TEST_OIDC_AUTH_ENDPOINT, - "token_endpoint": TEST_OIDC_TOKEN_ENDPOINT, - "userinfo_endpoint": TEST_OIDC_USERINFO_ENDPOINT, - "user_mapping_provider": {"config": {"localpart_template": "{{ user.sub }}"}}, -} |