Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

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

Merged
merged 33 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0f6487e
Drop v1 lookup
Vetchu Jul 11, 2022
ef1c794
Add changelog
Vetchu Jul 11, 2022
e9c789a
Address code review
Vetchu Jul 11, 2022
e354ec6
Repair tests PoC.
Vetchu Jul 11, 2022
eb5ba09
Update changelog.d/13241.removal
Vetchu Jul 12, 2022
72313f6
merge develop
Vetchu Jul 21, 2022
779e78c
Add tests, address concerns
Vetchu Jul 21, 2022
5668dcd
minify test
Vetchu Jul 22, 2022
6e908e4
missing comma
Vetchu Jul 25, 2022
242fa48
Merge branch 'develop' into misc/drop-v1-lookup
Vetchu Jul 25, 2022
6698279
Address comments
Vetchu Jul 25, 2022
48d88f0
Merge branch 'misc/drop-v1-lookup' of github.com:Vetchu/synapse into …
Vetchu Jul 25, 2022
6b1413b
Merge branch 'develop' into misc/drop-v1-lookup
Vetchu Jul 25, 2022
dacd33d
Merge branch 'matrix-org:develop' into misc/drop-v1-lookup
Vetchu Aug 11, 2022
9a04234
Update tests/rest/client/test_rooms.py
Vetchu Aug 15, 2022
e115f60
addressed the comments
Vetchu Aug 15, 2022
c535ea8
Merge branch 'develop' into misc/drop-v1-lookup
Vetchu Aug 15, 2022
f2a74b2
add missing check
Vetchu Aug 15, 2022
bb84fe4
Merge branch 'develop' into misc/drop-v1-lookup
Vetchu Aug 15, 2022
8ab4b35
Merge branch 'develop' into misc/drop-v1-lookup
Vetchu Aug 15, 2022
f9600bb
fixed test
Vetchu Aug 15, 2022
0073118
catch missing param for 3pid
Vetchu Aug 15, 2022
ebcdbba
synapseerror
Vetchu Aug 15, 2022
b97b27f
validate earlier
Vetchu Aug 15, 2022
23be3b9
merge develop
Vetchu Aug 23, 2022
d1b6ae8
Merge remote-tracking branch 'origin' into misc/drop-v1-lookup
Vetchu Aug 23, 2022
21d9be1
Next iteration
Vetchu Aug 23, 2022
caeee4d
allow exception to go up
Vetchu Aug 23, 2022
3792e8c
Merge branch 'develop' into misc/drop-v1-lookup
Vetchu Aug 24, 2022
3dfd8fb
beautify code
Vetchu Aug 25, 2022
5b237aa
hopefully addresses the comments
Vetchu Aug 26, 2022
27951c5
Update synapse/handlers/room.py
richvdh Aug 31, 2022
c6aa582
Merge branch 'develop' into misc/drop-v1-lookup
richvdh Aug 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/13241.removal
Original file line number Diff line number Diff line change
@@ -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.
147 changes: 29 additions & 118 deletions synapse/handlers/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,7 @@ async def proxy_msisdn_submit_token(
raise SynapseError(400, "Error contacting the identity server")

async def lookup_3pid(
self,
id_server: str,
medium: str,
address: str,
id_access_token: Optional[str] = None,
self, id_server: str, medium: str, address: str, id_access_token: str
) -> Optional[str]:
"""Looks up a 3pid in the passed identity server.

Expand All @@ -611,60 +607,15 @@ async def lookup_3pid(
Returns:
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. "[email protected]").

Returns:
the matrix ID of the 3pid, or None if it is not recognized.
"""
try:
data = await self.blacklisting_http_client.get_json(
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server),
{"medium": medium, "address": address},
results = await self._lookup_3pid_v2(
id_server, id_access_token, medium, address
)

if "mxid" in data:
# note: we used to verify the identity server's signature here, but no longer
# require or validate it. See the following for context:
# 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
return results
except Exception as e:
logger.warning("Error when looking up hashing details: %s", e)
return None

async def _lookup_3pid_v2(
self, id_server: str, id_access_token: str, medium: str, address: str
Expand Down Expand Up @@ -793,7 +744,7 @@ async def ask_id_server_for_third_party_invite(
room_type: Optional[str],
inviter_display_name: 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]:
"""
Asks an identity server for a third party invite.
Expand All @@ -814,7 +765,7 @@ async def ask_id_server_for_third_party_invite(
inviter_display_name: The current display name of the
inviter.
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

Returns:
Expand Down Expand Up @@ -846,71 +797,31 @@ async def ask_id_server_for_third_party_invite(
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
# Identity Service endpoints if id_access_token is present
# Identity Service endpoints
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" % (
id_server_scheme,
id_server,
key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % (
id_server_scheme,
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")
except HttpResponseException as e:
logger.info("Failed to POST %s with JSON: %s", url, e)
Vetchu marked this conversation as resolved.
Show resolved Hide resolved

# 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,
if not (data and data.get("token") and data.get("display_name")):
raise SynapseError(
500, "No data with token was received from identity 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"]
public_keys = data.get("public_keys", [])
if "public_key" in data:
Expand Down
15 changes: 13 additions & 2 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
from synapse.handlers.relations import BundledAggregations
from synapse.module_api import NOT_SPAM
from synapse.rest.admin._base import assert_user_is_admin
from synapse.rest.client.room import has_3pid_invite_keys
from synapse.storage.state import StateFilter
from synapse.streams import EventSource
from synapse.types import (
Expand Down Expand Up @@ -369,7 +370,9 @@ async def _update_upgraded_room_pls(
# required PL above that.

pl_content = copy_and_fixup_power_levels_contents(old_room_pl_state.content)
users_default: int = pl_content.get("users_default", 0) # type: ignore[assignment]
users_default: int = pl_content.get(
"users_default", 0
) # type: ignore[assignment]
restricted_level = max(users_default + 1, 50)

updated = False
Expand Down Expand Up @@ -732,6 +735,14 @@ async def create_room(
invite_3pid_list = config.get("invite_3pid", [])
invite_list = config.get("invite", [])

# validate each entry
for invite_3pid in invite_3pid_list:
try:
assert has_3pid_invite_keys(invite_3pid)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
except SynapseError as e:
logger.error(e, invite_3pid)
invite_3pid_list.remove(invite_list)
richvdh marked this conversation as resolved.
Show resolved Hide resolved

if not is_requester_admin:
spam_check = await self.spam_checker.user_may_create_room(user_id)
if spam_check != NOT_SPAM:
Expand Down Expand Up @@ -979,7 +990,7 @@ async def create_room(

for invite_3pid in invite_3pid_list:
id_server = invite_3pid["id_server"]
id_access_token = invite_3pid.get("id_access_token") # optional
id_access_token = invite_3pid["id_access_token"]
Vetchu marked this conversation as resolved.
Show resolved Hide resolved
address = invite_3pid["address"]
medium = invite_3pid["medium"]
# Note that do_3pid_invite can raise a ShadowBanError, but this was
Expand Down
6 changes: 3 additions & 3 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,7 @@ async def do_3pid_invite(
id_server: str,
requester: Requester,
txn_id: Optional[str],
id_access_token: Optional[str] = None,
id_access_token: str,
prev_event_ids: Optional[List[str]] = None,
depth: Optional[int] = None,
) -> Tuple[str, int]:
Expand All @@ -1397,7 +1397,7 @@ async def do_3pid_invite(
requester: The user making the request.
txn_id: The transaction ID this is part of, or None if this is not
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.
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
Expand Down Expand Up @@ -1494,7 +1494,7 @@ async def _make_and_store_3pid_invite(
room_id: str,
user: UserID,
txn_id: Optional[str],
id_access_token: Optional[str] = None,
id_access_token: str,
prev_event_ids: Optional[List[str]] = None,
depth: Optional[int] = None,
) -> Tuple[EventBase, int]:
Expand Down
24 changes: 16 additions & 8 deletions synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@
logger = logging.getLogger(__name__)


def has_3pid_invite_keys(content: JsonDict) -> bool:
# if the request has medium and address keys, we should treat it as a 3pid invite.
if all(key in content for key in ("medium", "address")):
if all(key in content for key in ("id_server", "id_access_token")):
return True
# if it does not have id_server or id_access_token, we should treat that as an error.
raise SynapseError(
400,
"`id_server` and `id_access_token` are required when doing 3pid invite",
Codes.MISSING_PARAM,
)
return False
richvdh marked this conversation as resolved.
Show resolved Hide resolved


class TransactionRestServlet(RestServlet):
def __init__(self, hs: "HomeServer"):
super().__init__()
Expand Down Expand Up @@ -860,7 +874,7 @@ async def on_POST(
# cheekily send invalid bodies.
content = {}

if membership_action == "invite" and self._has_3pid_invite_keys(content):
if membership_action == "invite" and has_3pid_invite_keys(content):
try:
await self.room_member_handler.do_3pid_invite(
room_id,
Expand All @@ -870,7 +884,7 @@ async def on_POST(
content["id_server"],
requester,
txn_id,
content.get("id_access_token"),
content["id_access_token"],
)
except ShadowBanError:
# Pretend the request succeeded.
Expand Down Expand Up @@ -907,12 +921,6 @@ async def on_POST(

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(
self, request: SynapseRequest, room_id: str, membership_action: str, txn_id: str
) -> Awaitable[Tuple[int, JsonDict]]:
Expand Down
1 change: 0 additions & 1 deletion synapse/rest/media/v1/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@

logger = logging.getLogger(__name__)


# How often to run the background job to update the "recently accessed"
# attribute of local and remote media.
UPDATE_RECENTLY_ACCESSED_TS = 60 * 1000 # 1 minute
Expand Down
3 changes: 1 addition & 2 deletions tests/rest/client/test_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@


class IdentityTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
]

def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:

config = self.default_config()
config["enable_3pid_lookup"] = False
self.hs = self.setup_test_homeserver(config=config)
Expand All @@ -54,6 +52,7 @@ def test_3pid_lookup_disabled(self) -> None:
"id_server": "testis",
"medium": "email",
"address": "[email protected]",
"id_access_token": tok,
}
request_url = ("/rooms/%s/invite" % (room_id)).encode("ascii")
channel = self.make_request(
Expand Down
18 changes: 18 additions & 0 deletions tests/rest/client/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3461,3 +3461,21 @@ def test_threepid_invite_spamcheck(self) -> None:

# Also check that it stopped before calling _make_and_store_3pid_invite.
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": "[email protected]",
},
access_token=self.tok,
)
self.assertEqual(channel.code, 400)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(channel.json_body["errcode"], "M_MISSING_PARAM")
7 changes: 6 additions & 1 deletion tests/rest/client/test_shadow_banned.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ def test_invite_3pid(self) -> None:
channel = self.make_request(
"POST",
"/rooms/%s/invite" % (room_id,),
{"id_server": "test", "medium": "email", "address": "[email protected]"},
{
"id_server": "test",
"medium": "email",
"address": "[email protected]",
"id_access_token": "anytoken",
},
access_token=self.banned_access_token,
)
self.assertEqual(200, channel.code, channel.result)
Expand Down