Drop support for calling /_matrix/client/v3/rooms/{roomId}/invite without an id_access_token (#13241)

Fixes #13206

Signed-off-by: Jacek Kusnierz jacek.kusnierz@tum.de
This commit is contained in:
Jacek Kuśnierz 2022-08-31 14:10:25 +02:00 committed by GitHub
parent 42b11d5565
commit 84ddcd7bbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 81 additions and 137 deletions

View file

@ -0,0 +1 @@
Drop support for calling `/_matrix/client/v3/rooms/{roomId}/invite` without an `id_access_token`, which was not permitted by the spec. Contributed by @Vetchu.

View file

@ -538,11 +538,7 @@ class IdentityHandler:
raise SynapseError(400, "Error contacting the identity server") raise SynapseError(400, "Error contacting the identity server")
async def lookup_3pid( async def lookup_3pid(
self, self, id_server: str, medium: str, address: str, id_access_token: str
id_server: str,
medium: str,
address: str,
id_access_token: Optional[str] = None,
) -> Optional[str]: ) -> Optional[str]:
"""Looks up a 3pid in the passed identity server. """Looks up a 3pid in the passed identity server.
@ -557,60 +553,15 @@ class IdentityHandler:
Returns: Returns:
the matrix ID of the 3pid, or None if it is not recognized. the matrix ID of the 3pid, or None if it is not recognized.
""" """
if id_access_token is not None:
try:
results = await self._lookup_3pid_v2(
id_server, id_access_token, medium, address
)
return results
except Exception as e:
# Catch HttpResponseExcept for a non-200 response code
# Check if this identity server does not know about v2 lookups
if isinstance(e, HttpResponseException) and e.code == 404:
# This is an old identity server that does not yet support v2 lookups
logger.warning(
"Attempted v2 lookup on v1 identity server %s. Falling "
"back to v1",
id_server,
)
else:
logger.warning("Error when looking up hashing details: %s", e)
return None
return await self._lookup_3pid_v1(id_server, medium, address)
async def _lookup_3pid_v1(
self, id_server: str, medium: str, address: str
) -> Optional[str]:
"""Looks up a 3pid in the passed identity server using v1 lookup.
Args:
id_server: The server name (including port, if required)
of the identity server to use.
medium: The type of the third party identifier (e.g. "email").
address: The third party identifier (e.g. "foo@example.com").
Returns:
the matrix ID of the 3pid, or None if it is not recognized.
"""
try: try:
data = await self.blacklisting_http_client.get_json( results = await self._lookup_3pid_v2(
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server), id_server, id_access_token, medium, address
{"medium": medium, "address": address},
) )
return results
if "mxid" in data: except Exception as e:
# note: we used to verify the identity server's signature here, but no longer logger.warning("Error when looking up hashing details: %s", e)
# require or validate it. See the following for context: return None
# https://github.com/matrix-org/synapse/issues/5253#issuecomment-666246950
return data["mxid"]
except RequestTimedOutError:
raise SynapseError(500, "Timed out contacting identity server")
except OSError as e:
logger.warning("Error from v1 identity server lookup: %s" % (e,))
return None
async def _lookup_3pid_v2( async def _lookup_3pid_v2(
self, id_server: str, id_access_token: str, medium: str, address: str self, id_server: str, id_access_token: str, medium: str, address: str
@ -739,7 +690,7 @@ class IdentityHandler:
room_type: Optional[str], room_type: Optional[str],
inviter_display_name: str, inviter_display_name: str,
inviter_avatar_url: str, inviter_avatar_url: str,
id_access_token: Optional[str] = None, id_access_token: str,
) -> Tuple[str, List[Dict[str, str]], Dict[str, str], str]: ) -> Tuple[str, List[Dict[str, str]], Dict[str, str], str]:
""" """
Asks an identity server for a third party invite. Asks an identity server for a third party invite.
@ -760,7 +711,7 @@ class IdentityHandler:
inviter_display_name: The current display name of the inviter_display_name: The current display name of the
inviter. inviter.
inviter_avatar_url: The URL of the inviter's avatar. inviter_avatar_url: The URL of the inviter's avatar.
id_access_token (str|None): The access token to authenticate to the identity id_access_token (str): The access token to authenticate to the identity
server with server with
Returns: Returns:
@ -792,71 +743,24 @@ class IdentityHandler:
invite_config["org.matrix.web_client_location"] = self._web_client_location invite_config["org.matrix.web_client_location"] = self._web_client_location
# Add the identity service access token to the JSON body and use the v2 # Add the identity service access token to the JSON body and use the v2
# Identity Service endpoints if id_access_token is present # Identity Service endpoints
data = None data = None
base_url = "%s%s/_matrix/identity" % (id_server_scheme, id_server)
if id_access_token: key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % (
key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % ( id_server_scheme,
id_server_scheme, id_server,
id_server, )
url = "%s%s/_matrix/identity/v2/store-invite" % (id_server_scheme, id_server)
try:
data = await self.blacklisting_http_client.post_json_get_json(
url,
invite_config,
{"Authorization": create_id_access_token_header(id_access_token)},
) )
except RequestTimedOutError:
raise SynapseError(500, "Timed out contacting identity server")
# Attempt a v2 lookup
url = base_url + "/v2/store-invite"
try:
data = await self.blacklisting_http_client.post_json_get_json(
url,
invite_config,
{"Authorization": create_id_access_token_header(id_access_token)},
)
except RequestTimedOutError:
raise SynapseError(500, "Timed out contacting identity server")
except HttpResponseException as e:
if e.code != 404:
logger.info("Failed to POST %s with JSON: %s", url, e)
raise e
if data is None:
key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
id_server_scheme,
id_server,
)
url = base_url + "/api/v1/store-invite"
try:
data = await self.blacklisting_http_client.post_json_get_json(
url, invite_config
)
except RequestTimedOutError:
raise SynapseError(500, "Timed out contacting identity server")
except HttpResponseException as e:
logger.warning(
"Error trying to call /store-invite on %s%s: %s",
id_server_scheme,
id_server,
e,
)
if data is None:
# Some identity servers may only support application/x-www-form-urlencoded
# types. This is especially true with old instances of Sydent, see
# https://github.com/matrix-org/sydent/pull/170
try:
data = await self.blacklisting_http_client.post_urlencoded_get_json(
url, invite_config
)
except HttpResponseException as e:
logger.warning(
"Error calling /store-invite on %s%s with fallback "
"encoding: %s",
id_server_scheme,
id_server,
e,
)
raise e
# TODO: Check for success
token = data["token"] token = data["token"]
public_keys = data.get("public_keys", []) public_keys = data.get("public_keys", [])
if "public_key" in data: if "public_key" in data:

View file

@ -19,6 +19,7 @@ import math
import random import random
import string import string
from collections import OrderedDict from collections import OrderedDict
from http import HTTPStatus
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -704,8 +705,8 @@ class RoomCreationHandler:
was, requested, `room_alias`. Secondly, the stream_id of the was, requested, `room_alias`. Secondly, the stream_id of the
last persisted event. last persisted event.
Raises: Raises:
SynapseError if the room ID couldn't be stored, or something went SynapseError if the room ID couldn't be stored, 3pid invitation config
horribly wrong. validation failed, or something went horribly wrong.
ResourceLimitError if server is blocked to some resource being ResourceLimitError if server is blocked to some resource being
exceeded exceeded
""" """
@ -731,6 +732,19 @@ class RoomCreationHandler:
invite_3pid_list = config.get("invite_3pid", []) invite_3pid_list = config.get("invite_3pid", [])
invite_list = config.get("invite", []) invite_list = config.get("invite", [])
# validate each entry for correctness
for invite_3pid in invite_3pid_list:
if not all(
key in invite_3pid
for key in ("medium", "address", "id_server", "id_access_token")
):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"all of `medium`, `address`, `id_server` and `id_access_token` "
"are required when making a 3pid invite",
Codes.MISSING_PARAM,
)
if not is_requester_admin: if not is_requester_admin:
spam_check = await self.spam_checker.user_may_create_room(user_id) spam_check = await self.spam_checker.user_may_create_room(user_id)
if spam_check != NOT_SPAM: if spam_check != NOT_SPAM:
@ -978,7 +992,7 @@ class RoomCreationHandler:
for invite_3pid in invite_3pid_list: for invite_3pid in invite_3pid_list:
id_server = invite_3pid["id_server"] id_server = invite_3pid["id_server"]
id_access_token = invite_3pid.get("id_access_token") # optional id_access_token = invite_3pid["id_access_token"]
address = invite_3pid["address"] address = invite_3pid["address"]
medium = invite_3pid["medium"] medium = invite_3pid["medium"]
# Note that do_3pid_invite can raise a ShadowBanError, but this was # Note that do_3pid_invite can raise a ShadowBanError, but this was

View file

@ -1382,7 +1382,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
id_server: str, id_server: str,
requester: Requester, requester: Requester,
txn_id: Optional[str], txn_id: Optional[str],
id_access_token: Optional[str] = None, id_access_token: str,
prev_event_ids: Optional[List[str]] = None, prev_event_ids: Optional[List[str]] = None,
depth: Optional[int] = None, depth: Optional[int] = None,
) -> Tuple[str, int]: ) -> Tuple[str, int]:
@ -1397,7 +1397,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
requester: The user making the request. requester: The user making the request.
txn_id: The transaction ID this is part of, or None if this is not txn_id: The transaction ID this is part of, or None if this is not
part of a transaction. part of a transaction.
id_access_token: The optional identity server access token. id_access_token: Identity server access token.
depth: Override the depth used to order the event in the DAG. depth: Override the depth used to order the event in the DAG.
prev_event_ids: The event IDs to use as the prev events prev_event_ids: The event IDs to use as the prev events
Should normally be set to None, which will cause the depth to be calculated Should normally be set to None, which will cause the depth to be calculated
@ -1494,7 +1494,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
room_id: str, room_id: str,
user: UserID, user: UserID,
txn_id: Optional[str], txn_id: Optional[str],
id_access_token: Optional[str] = None, id_access_token: str,
prev_event_ids: Optional[List[str]] = None, prev_event_ids: Optional[List[str]] = None,
depth: Optional[int] = None, depth: Optional[int] = None,
) -> Tuple[EventBase, int]: ) -> Tuple[EventBase, int]:

View file

@ -17,6 +17,7 @@
import logging import logging
import re import re
from enum import Enum from enum import Enum
from http import HTTPStatus
from typing import TYPE_CHECKING, Awaitable, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Awaitable, Dict, List, Optional, Tuple
from urllib import parse as urlparse from urllib import parse as urlparse
@ -947,7 +948,16 @@ class RoomMembershipRestServlet(TransactionRestServlet):
# cheekily send invalid bodies. # cheekily send invalid bodies.
content = {} content = {}
if membership_action == "invite" and self._has_3pid_invite_keys(content): if membership_action == "invite" and all(
key in content for key in ("medium", "address")
):
if not all(key in content for key in ("id_server", "id_access_token")):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"`id_server` and `id_access_token` are required when doing 3pid invite",
Codes.MISSING_PARAM,
)
try: try:
await self.room_member_handler.do_3pid_invite( await self.room_member_handler.do_3pid_invite(
room_id, room_id,
@ -957,7 +967,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
content["id_server"], content["id_server"],
requester, requester,
txn_id, txn_id,
content.get("id_access_token"), content["id_access_token"],
) )
except ShadowBanError: except ShadowBanError:
# Pretend the request succeeded. # Pretend the request succeeded.
@ -994,12 +1004,6 @@ class RoomMembershipRestServlet(TransactionRestServlet):
return 200, return_value return 200, return_value
def _has_3pid_invite_keys(self, content: JsonDict) -> bool:
for key in {"id_server", "medium", "address"}:
if key not in content:
return False
return True
def on_PUT( def on_PUT(
self, request: SynapseRequest, room_id: str, membership_action: str, txn_id: str self, request: SynapseRequest, room_id: str, membership_action: str, txn_id: str
) -> Awaitable[Tuple[int, JsonDict]]: ) -> Awaitable[Tuple[int, JsonDict]]:

View file

@ -64,7 +64,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# How often to run the background job to update the "recently accessed" # How often to run the background job to update the "recently accessed"
# attribute of local and remote media. # attribute of local and remote media.
UPDATE_RECENTLY_ACCESSED_TS = 60 * 1000 # 1 minute UPDATE_RECENTLY_ACCESSED_TS = 60 * 1000 # 1 minute

View file

@ -25,7 +25,6 @@ from tests import unittest
class IdentityTestCase(unittest.HomeserverTestCase): class IdentityTestCase(unittest.HomeserverTestCase):
servlets = [ servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource, synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets, room.register_servlets,
@ -33,7 +32,6 @@ class IdentityTestCase(unittest.HomeserverTestCase):
] ]
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
config = self.default_config() config = self.default_config()
config["enable_3pid_lookup"] = False config["enable_3pid_lookup"] = False
self.hs = self.setup_test_homeserver(config=config) self.hs = self.setup_test_homeserver(config=config)
@ -54,6 +52,7 @@ class IdentityTestCase(unittest.HomeserverTestCase):
"id_server": "testis", "id_server": "testis",
"medium": "email", "medium": "email",
"address": "test@example.com", "address": "test@example.com",
"id_access_token": tok,
} }
request_url = ("/rooms/%s/invite" % (room_id)).encode("ascii") request_url = ("/rooms/%s/invite" % (room_id)).encode("ascii")
channel = self.make_request( channel = self.make_request(

View file

@ -3461,3 +3461,21 @@ class ThreepidInviteTestCase(unittest.HomeserverTestCase):
# Also check that it stopped before calling _make_and_store_3pid_invite. # Also check that it stopped before calling _make_and_store_3pid_invite.
make_invite_mock.assert_called_once() make_invite_mock.assert_called_once()
def test_400_missing_param_without_id_access_token(self) -> None:
"""
Test that a 3pid invite request returns 400 M_MISSING_PARAM
if we do not include id_access_token.
"""
channel = self.make_request(
method="POST",
path="/rooms/" + self.room_id + "/invite",
content={
"id_server": "example.com",
"medium": "email",
"address": "teresa@example.com",
},
access_token=self.tok,
)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.json_body["errcode"], "M_MISSING_PARAM")

View file

@ -97,7 +97,12 @@ class RoomTestCase(_ShadowBannedBase):
channel = self.make_request( channel = self.make_request(
"POST", "POST",
"/rooms/%s/invite" % (room_id,), "/rooms/%s/invite" % (room_id,),
{"id_server": "test", "medium": "email", "address": "test@test.test"}, {
"id_server": "test",
"medium": "email",
"address": "test@test.test",
"id_access_token": "anytoken",
},
access_token=self.banned_access_token, access_token=self.banned_access_token,
) )
self.assertEqual(200, channel.code, channel.result) self.assertEqual(200, channel.code, channel.result)