From 1f0f9abbeef655a90c2eaf95836ac1398536e5f2 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sat, 7 Mar 2020 16:38:26 +0100 Subject: [PATCH 01/11] Admin API to join users to a room. Similar to `auto_join_rooms` for creation of users. --- changelog.d/7049.feature | 1 + docs/admin_api/room_membership.md | 31 ++++++ synapse/rest/admin/__init__.py | 7 +- synapse/rest/admin/rooms.py | 65 ++++++++++- tests/rest/admin/test_room.py | 176 ++++++++++++++++++++++++++++++ 5 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 changelog.d/7049.feature create mode 100644 docs/admin_api/room_membership.md create mode 100644 tests/rest/admin/test_room.py diff --git a/changelog.d/7049.feature b/changelog.d/7049.feature new file mode 100644 index 000000000000..9bbbbe59ed56 --- /dev/null +++ b/changelog.d/7049.feature @@ -0,0 +1 @@ +Admin API `POST /_synapse/admin/v1/join/` to join users to a room. Similar to `auto_join_rooms` for creation of users. \ No newline at end of file diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md new file mode 100644 index 000000000000..330de8ffc986 --- /dev/null +++ b/docs/admin_api/room_membership.md @@ -0,0 +1,31 @@ +# Edit Room Membership API + +The API allow an administrator to join an user account with a specific `user_id` +to a room with a specific `roomIdOrAlias`. +You can only modify local users. + +## Parameters + +The following parameters are available: + +* `user_id` - Fully qualified user: for example, `@user:server.com`. +* `roomIdOrAlias` - The room identifier or alias to join: for example, `!636q39766251:server.com`. + +## Usage + +``` +POST /_synapse/admin/v1/join/ + +{ + "user_id": "@user:server.com" +} +``` +Including an `access_token` of a server admin. + +Response: + +``` +{ + "room_id": "!636q39766251:server.com" +} +``` diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 42cc2b062a58..ed70d448a141 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -29,7 +29,11 @@ from synapse.rest.admin.groups import DeleteGroupAdminRestServlet from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet -from synapse.rest.admin.rooms import ListRoomRestServlet, ShutdownRoomRestServlet +from synapse.rest.admin.rooms import ( + JoinRoomAliasServlet, + ListRoomRestServlet, + ShutdownRoomRestServlet, +) from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet from synapse.rest.admin.users import ( AccountValidityRenewServlet, @@ -189,6 +193,7 @@ def register_servlets(hs, http_server): """ register_servlets_for_client_rest_resource(hs, http_server) ListRoomRestServlet(hs).register(http_server) + JoinRoomAliasServlet(hs).register(http_server) PurgeRoomServlet(hs).register(http_server) SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index f9b8c0a4f0f3..c68595a14d9a 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -15,7 +15,7 @@ import logging from synapse.api.constants import Membership -from synapse.api.errors import Codes, SynapseError +from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.http.servlet import ( RestServlet, assert_params_in_dict, @@ -29,7 +29,7 @@ historical_admin_path_patterns, ) from synapse.storage.data_stores.main.room import RoomSortOrder -from synapse.types import create_requester +from synapse.types import RoomAlias, RoomID, UserID, create_requester from synapse.util.async_helpers import maybe_awaitable logger = logging.getLogger(__name__) @@ -237,3 +237,64 @@ async def on_GET(self, request): response["prev_batch"] = 0 return 200, response + + +class JoinRoomAliasServlet(RestServlet): + + PATTERNS = admin_patterns("/join/(?P[^/]*)") + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.room_member_handler = hs.get_room_member_handler() + self.admin_handler = hs.get_handlers().admin_handler + + async def on_POST(self, request, room_identifier): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + try: + content = parse_json_object_from_request(request) + except Exception: + # Turns out we used to ignore the body entirely, and some clients + # cheekily send invalid bodies. + content = {} + + assert_params_in_dict(content, ["user_id"]) + target_user = UserID.from_string(content["user_id"]) + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "This endpoint can only be used with local users") + + if not await self.admin_handler.get_user(target_user): + raise NotFoundError("User not found") + + if RoomID.is_valid(room_identifier): + room_id = room_identifier + try: + remote_room_hosts = [ + x.decode("ascii") for x in request.args[b"server_name"] + ] # type: Optional[List[str]] + except Exception: + remote_room_hosts = None + elif RoomAlias.is_valid(room_identifier): + handler = self.room_member_handler + room_alias = RoomAlias.from_string(room_identifier) + room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias) + room_id = room_id.to_string() + else: + raise SynapseError( + 400, "%s was not legal room ID or room alias" % (room_identifier,) + ) + + fake_requester = create_requester(target_user) + await self.room_member_handler.update_membership( + requester=fake_requester, + target=fake_requester.user, + room_id=room_id, + action="join", + remote_room_hosts=remote_room_hosts, + ratelimit=False, + ) + + return 200, {"room_id": room_id} diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py new file mode 100644 index 000000000000..b27eebb716e7 --- /dev/null +++ b/tests/rest/admin/test_room.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Dirk Klimpel +# +# 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 synapse.rest.admin +from synapse.rest.client.v1 import login, room + +from tests import unittest + +"""Tests admin REST events for /rooms paths.""" + + +class JoinAliasRoomTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, homeserver): + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + 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) + self.url = "/_synapse/admin/v1/join/{}".format(self.room_id) + + def test_requester_is_no_admin(self): + """ + If the user is not a server admin, an error 403 is returned. + """ + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.second_tok, + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("You are not a server admin", channel.json_body["error"]) + + def test_invalid_parameter(self): + """ + If a parameter is missing, return an error + """ + body = json.dumps({"unknown_parameter": "@unknown:test"}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("Missing params: ['user_id']", channel.json_body["error"]) + + def test_local_user_does_not_exist(self): + """ + Tests that a lookup for a user that does not exist returns a 404 + """ + body = json.dumps({"user_id": "@unknown:test"}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("User not found", channel.json_body["error"]) + + def test_remote_user(self): + """ + Check that only local user can join rooms. + """ + body = json.dumps({"user_id": "@not:exist.bla"}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("This endpoint can only be used with local users", channel.json_body["error"]) + + def test_room_does_not_exist(self): + """ + Check that unknown rooms/server return error 404. + """ + body = json.dumps({"user_id": self.second_user_id}) + url = "/_synapse/admin/v1/join/!unknown:test" + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("No known servers", channel.json_body["error"]) + + def test_room_is_not_valid(self): + """ + Check that invalid room names, return an error 400. + """ + body = json.dumps({"user_id": self.second_user_id}) + url = "/_synapse/admin/v1/join/invalidroom" + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual("invalidroom was not legal room ID or room alias", channel.json_body["error"]) + + def test_join_room(self): + """ + Test joining a local user to a room. + """ + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + self.url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(self.room_id, channel.json_body["room_id"]) + + # Validate status in room + request, channel = self.make_request( + "GET", + "/rooms/%s/members?membership=join" % self.room_id, + access_token=self.second_tok + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(self.second_user_id, channel.json_body["chunk"][1]["user_id"]) From dffe1dcb3cda6fb776c035c48375a7d17de3182f Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sat, 7 Mar 2020 16:47:01 +0100 Subject: [PATCH 02/11] Update Changelog + linr --- changelog.d/7049.feature | 1 - changelog.d/7051.feature | 1 + synapse/rest/admin/rooms.py | 2 +- tests/rest/admin/test_room.py | 12 +++++++++--- 4 files changed, 11 insertions(+), 5 deletions(-) delete mode 100644 changelog.d/7049.feature create mode 100644 changelog.d/7051.feature diff --git a/changelog.d/7049.feature b/changelog.d/7049.feature deleted file mode 100644 index 9bbbbe59ed56..000000000000 --- a/changelog.d/7049.feature +++ /dev/null @@ -1 +0,0 @@ -Admin API `POST /_synapse/admin/v1/join/` to join users to a room. Similar to `auto_join_rooms` for creation of users. \ No newline at end of file diff --git a/changelog.d/7051.feature b/changelog.d/7051.feature new file mode 100644 index 000000000000..3e36a3f65e40 --- /dev/null +++ b/changelog.d/7051.feature @@ -0,0 +1 @@ +Admin API `POST /_synapse/admin/v1/join/` to join users to a room like `auto_join_rooms` for creation of users. \ No newline at end of file diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index c68595a14d9a..2c794626642f 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -274,7 +274,7 @@ async def on_POST(self, request, room_identifier): try: remote_room_hosts = [ x.decode("ascii") for x in request.args[b"server_name"] - ] # type: Optional[List[str]] + ] except Exception: remote_room_hosts = None elif RoomAlias.is_valid(room_identifier): diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index b27eebb716e7..e590104c460e 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -110,7 +110,10 @@ def test_remote_user(self): self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("This endpoint can only be used with local users", channel.json_body["error"]) + self.assertEqual( + "This endpoint can only be used with local users", + channel.json_body["error"], + ) def test_room_does_not_exist(self): """ @@ -146,7 +149,10 @@ def test_room_is_not_valid(self): self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("invalidroom was not legal room ID or room alias", channel.json_body["error"]) + self.assertEqual( + "invalidroom was not legal room ID or room alias", + channel.json_body["error"], + ) def test_join_room(self): """ @@ -169,7 +175,7 @@ def test_join_room(self): request, channel = self.make_request( "GET", "/rooms/%s/members?membership=join" % self.room_id, - access_token=self.second_tok + access_token=self.second_tok, ) self.render(request) self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) From 3825d67eb8e6f5165e9cd87bb62000ed837bc706 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sat, 7 Mar 2020 16:54:50 +0100 Subject: [PATCH 03/11] fix --- synapse/rest/admin/rooms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 2c794626642f..85c497bd9589 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import List, Optional from synapse.api.constants import Membership from synapse.api.errors import Codes, NotFoundError, SynapseError @@ -274,7 +275,7 @@ async def on_POST(self, request, room_identifier): try: remote_room_hosts = [ x.decode("ascii") for x in request.args[b"server_name"] - ] + ] # type: Optional[List[str]] except Exception: remote_room_hosts = None elif RoomAlias.is_valid(room_identifier): From a4cef4e648fe21a5960307ce115b639a23228e36 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sat, 7 Mar 2020 17:46:29 +0100 Subject: [PATCH 04/11] update unit test --- tests/rest/admin/test_room.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index e590104c460e..48bbef5f6e41 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -171,12 +171,12 @@ def test_join_room(self): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(self.room_id, channel.json_body["room_id"]) - # Validate status in room + # Validate if user is member of room request, channel = self.make_request( "GET", - "/rooms/%s/members?membership=join" % self.room_id, + "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, ) self.render(request) self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(self.second_user_id, channel.json_body["chunk"][1]["user_id"]) + self.assertEqual(self.room_id, channel.json_body["joined_rooms"][0]) From eb6f072032b82864e9e4ecb2826da3872f80f1a2 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Sat, 7 Mar 2020 17:49:18 +0100 Subject: [PATCH 05/11] lint --- tests/rest/admin/test_room.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 48bbef5f6e41..5a0ddce2f315 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -173,9 +173,7 @@ def test_join_room(self): # Validate if user is member of room request, channel = self.make_request( - "GET", - "/_matrix/client/r0/joined_rooms", - access_token=self.second_tok, + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, ) self.render(request) self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) From a0cab45a4a77041936aa3bd4031f45d8dcfbe1d8 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 17 Mar 2020 21:33:12 +0100 Subject: [PATCH 06/11] Update doc, add tests and "invite" for private rooms. --- docs/admin_api/room_membership.md | 3 ++ synapse/rest/admin/rooms.py | 18 +++++++- tests/rest/admin/test_room.py | 72 +++++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md index 330de8ffc986..49d433e339da 100644 --- a/docs/admin_api/room_membership.md +++ b/docs/admin_api/room_membership.md @@ -3,6 +3,9 @@ The API allow an administrator to join an user account with a specific `user_id` to a room with a specific `roomIdOrAlias`. You can only modify local users. +The room must have join rule `JoinRules.PUBLIC`. It is default for public rooms. +The administrator must be admin or creator of a room, if the room has join rule +`JoinRules.INVITE` (default for private rooms). ## Parameters diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 85c497bd9589..7263348768ad 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -15,7 +15,7 @@ import logging from typing import List, Optional -from synapse.api.constants import Membership +from synapse.api.constants import EventTypes, JoinRules, Membership from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.http.servlet import ( RestServlet, @@ -249,6 +249,7 @@ def __init__(self, hs): self.auth = hs.get_auth() self.room_member_handler = hs.get_room_member_handler() self.admin_handler = hs.get_handlers().admin_handler + self.state_handler = hs.get_state_handler() async def on_POST(self, request, room_identifier): requester = await self.auth.get_user_by_req(request) @@ -289,6 +290,21 @@ async def on_POST(self, request, room_identifier): ) fake_requester = create_requester(target_user) + + # send invite if room has "JoinRules.INVITE" + room_state = await self.state_handler.get_current_state(room_id) + join_rules_event = room_state.get((EventTypes.JoinRules, "")) + if join_rules_event: + if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC): + await self.room_member_handler.update_membership( + requester=requester, + target=fake_requester.user, + room_id=room_id, + action="invite", + remote_room_hosts=remote_room_hosts, + ratelimit=False, + ) + await self.room_member_handler.update_membership( requester=fake_requester, target=fake_requester.user, diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 5a0ddce2f315..3a0458d83d93 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -16,6 +16,7 @@ import json import synapse.rest.admin +from synapse.api.errors import Codes from synapse.rest.client.v1 import login, room from tests import unittest @@ -41,8 +42,10 @@ def prepare(self, reactor, clock, homeserver): 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) - self.url = "/_synapse/admin/v1/join/{}".format(self.room_id) + self.public_room_id = self.helper.create_room_as( + self.creator, tok=self.creator_tok, is_public=True + ) + self.url = "/_synapse/admin/v1/join/{}".format(self.public_room_id) def test_requester_is_no_admin(self): """ @@ -59,7 +62,7 @@ def test_requester_is_no_admin(self): self.render(request) self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("You are not a server admin", channel.json_body["error"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) def test_invalid_parameter(self): """ @@ -76,7 +79,7 @@ def test_invalid_parameter(self): self.render(request) self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("Missing params: ['user_id']", channel.json_body["error"]) + self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) def test_local_user_does_not_exist(self): """ @@ -93,7 +96,7 @@ def test_local_user_does_not_exist(self): self.render(request) self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual("User not found", channel.json_body["error"]) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) def test_remote_user(self): """ @@ -154,9 +157,9 @@ def test_room_is_not_valid(self): channel.json_body["error"], ) - def test_join_room(self): + def test_join_public_room(self): """ - Test joining a local user to a room. + Test joining a local user to a public room with "JoinRules.PUBLIC" """ body = json.dumps({"user_id": self.second_user_id}) @@ -169,7 +172,58 @@ def test_join_room(self): self.render(request) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(self.room_id, channel.json_body["room_id"]) + self.assertEqual(self.public_room_id, channel.json_body["room_id"]) + + # Validate if user is member of room + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(self.public_room_id, channel.json_body["joined_rooms"][0]) + + def test_join_private_room(self): + """ + Test joining a local user to a private room with "JoinRules.INVITE" + """ + private_room_id = self.helper.create_room_as( + self.creator, tok=self.creator_tok, is_public=False + ) + url = "/_synapse/admin/v1/join/{}".format(private_room_id) + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_join_private_room_if_owner(self): + """ + Test joining a local user to a private room with "JoinRules.INVITE", + when admin is owner of this room. + """ + private_room_id = self.helper.create_room_as( + self.admin_user, tok=self.admin_user_tok, is_public=False + ) + url = "/_synapse/admin/v1/join/{}".format(private_room_id) + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["room_id"]) # Validate if user is member of room request, channel = self.make_request( @@ -177,4 +231,4 @@ def test_join_room(self): ) self.render(request) self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) - self.assertEqual(self.room_id, channel.json_body["joined_rooms"][0]) + self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) From 7f755f6805aa45b09a9c391f19d90774c9a1d5a7 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 23 Mar 2020 22:24:35 +0100 Subject: [PATCH 07/11] Apply suggestions from code review Co-Authored-By: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- tests/rest/admin/test_room.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 3a0458d83d93..61a359957554 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -174,7 +174,8 @@ def test_join_public_room(self): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(self.public_room_id, channel.json_body["room_id"]) - # Validate if user is member of room + # Validate if user is a member of the room + request, channel = self.make_request( "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, ) @@ -206,7 +207,8 @@ def test_join_private_room(self): def test_join_private_room_if_owner(self): """ Test joining a local user to a private room with "JoinRules.INVITE", - when admin is owner of this room. + when server admin is owner of this room. + """ private_room_id = self.helper.create_room_as( self.admin_user, tok=self.admin_user_tok, is_public=False @@ -225,7 +227,8 @@ def test_join_private_room_if_owner(self): self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(private_room_id, channel.json_body["room_id"]) - # Validate if user is member of room + # Validate if user is a member of the room + request, channel = self.make_request( "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, ) From 8db367d619a13580e1d4fd0f043197e327b325ab Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 24 Mar 2020 22:04:27 +0100 Subject: [PATCH 08/11] update docs an add a test --- docs/admin_api/room_membership.md | 13 ++++---- synapse/rest/admin/rooms.py | 7 +--- tests/rest/admin/test_room.py | 53 +++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md index 49d433e339da..0a5ecd9acd77 100644 --- a/docs/admin_api/room_membership.md +++ b/docs/admin_api/room_membership.md @@ -1,11 +1,12 @@ # Edit Room Membership API -The API allow an administrator to join an user account with a specific `user_id` -to a room with a specific `roomIdOrAlias`. -You can only modify local users. -The room must have join rule `JoinRules.PUBLIC`. It is default for public rooms. -The administrator must be admin or creator of a room, if the room has join rule -`JoinRules.INVITE` (default for private rooms). +The API allows an administrator to join an user account with a given `user_id` +to a room with a given `roomIdOrAlias`. You can only modify the membership of +local users. The room must have join rule `JoinRules.PUBLIC`, which is the +default for public rooms. If the room has the join rule `JoinRules.INVITE` +(default for private rooms), the server administrator must have permissions +to invite users to this room. Per default you can invite users if you are +member of a room. ## Parameters diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 7263348768ad..659b8a10ee2d 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -255,12 +255,7 @@ async def on_POST(self, request, room_identifier): requester = await self.auth.get_user_by_req(request) await assert_user_is_admin(self.auth, requester.user) - try: - content = parse_json_object_from_request(request) - except Exception: - # Turns out we used to ignore the body entirely, and some clients - # cheekily send invalid bodies. - content = {} + content = parse_json_object_from_request(request) assert_params_in_dict(content, ["user_id"]) target_user = UserID.from_string(content["user_id"]) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 61a359957554..407dd3ac1efb 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -183,9 +183,10 @@ def test_join_public_room(self): self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(self.public_room_id, channel.json_body["joined_rooms"][0]) - def test_join_private_room(self): + def test_join_private_room_if_not_member(self): """ Test joining a local user to a private room with "JoinRules.INVITE" + when server admin is not member of this room. """ private_room_id = self.helper.create_room_as( self.creator, tok=self.creator_tok, is_public=False @@ -204,11 +205,59 @@ def test_join_private_room(self): self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + def test_join_private_room_if_member(self): + """ + Test joining a local user to a private room with "JoinRules.INVITE", + when server admin is member of this room. + """ + private_room_id = self.helper.create_room_as( + self.creator, tok=self.creator_tok, is_public=False + ) + self.helper.invite( + room=private_room_id, + src=self.creator, + targ=self.admin_user, + tok=self.creator_tok, + ) + self.helper.join(room=private_room_id, user=self.admin_user, tok=self.admin_user_tok) + + # Validate if server admin is a member of the room + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) + + # Join user to room. + + url = "/_synapse/admin/v1/join/{}".format(private_room_id) + body = json.dumps({"user_id": self.second_user_id}) + + request, channel = self.make_request( + "POST", + url, + content=body.encode(encoding="utf_8"), + access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["room_id"]) + + # Validate if user is a member of the room + + request, channel = self.make_request( + "GET", "/_matrix/client/r0/joined_rooms", access_token=self.second_tok, + ) + self.render(request) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) + def test_join_private_room_if_owner(self): """ Test joining a local user to a private room with "JoinRules.INVITE", when server admin is owner of this room. - """ private_room_id = self.helper.create_room_as( self.admin_user, tok=self.admin_user_tok, is_public=False From 937b7d093d24c1860b92e9c89ef76d15178fc6bb Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 24 Mar 2020 22:07:30 +0100 Subject: [PATCH 09/11] code style --- tests/rest/admin/test_room.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 407dd3ac1efb..672cc3eac521 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -219,7 +219,9 @@ def test_join_private_room_if_member(self): targ=self.admin_user, tok=self.creator_tok, ) - self.helper.join(room=private_room_id, user=self.admin_user, tok=self.admin_user_tok) + self.helper.join( + room=private_room_id, user=self.admin_user, tok=self.admin_user_tok + ) # Validate if server admin is a member of the room From c56d7ca45b68c7cbed7e23fcb918344472c1100a Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 26 Mar 2020 13:49:35 +0100 Subject: [PATCH 10/11] Update room_membership.md --- docs/admin_api/room_membership.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md index 0a5ecd9acd77..3b3a67458d45 100644 --- a/docs/admin_api/room_membership.md +++ b/docs/admin_api/room_membership.md @@ -1,24 +1,21 @@ # Edit Room Membership API -The API allows an administrator to join an user account with a given `user_id` +This API allows an administrator to join an user account with a given `user_id` to a room with a given `roomIdOrAlias`. You can only modify the membership of -local users. The room must have join rule `JoinRules.PUBLIC`, which is the -default for public rooms. If the room has the join rule `JoinRules.INVITE` -(default for private rooms), the server administrator must have permissions -to invite users to this room. Per default you can invite users if you are -member of a room. +local users. The server administrator must have permissions to invite users to the +room. ## Parameters The following parameters are available: * `user_id` - Fully qualified user: for example, `@user:server.com`. -* `roomIdOrAlias` - The room identifier or alias to join: for example, `!636q39766251:server.com`. +* `room_id_or_alias` - The room identifier or alias to join: for example, `!636q39766251:server.com`. ## Usage ``` -POST /_synapse/admin/v1/join/ +POST /_synapse/admin/v1/join/ { "user_id": "@user:server.com" From d2a5612b7baf400fe05ec0a436ab53caf3f97d20 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 27 Mar 2020 15:24:09 +0000 Subject: [PATCH 11/11] Minor formatting --- docs/admin_api/room_membership.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md index 3b3a67458d45..16736d3d37c7 100644 --- a/docs/admin_api/room_membership.md +++ b/docs/admin_api/room_membership.md @@ -1,16 +1,17 @@ # Edit Room Membership API This API allows an administrator to join an user account with a given `user_id` -to a room with a given `roomIdOrAlias`. You can only modify the membership of -local users. The server administrator must have permissions to invite users to the -room. +to a room with a given `room_id_or_alias`. You can only modify the membership of +local users. The server administrator must be in the room and have permission to +invite users. ## Parameters The following parameters are available: * `user_id` - Fully qualified user: for example, `@user:server.com`. -* `room_id_or_alias` - The room identifier or alias to join: for example, `!636q39766251:server.com`. +* `room_id_or_alias` - The room identifier or alias to join: for example, + `!636q39766251:server.com`. ## Usage @@ -21,6 +22,7 @@ POST /_synapse/admin/v1/join/ "user_id": "@user:server.com" } ``` + Including an `access_token` of a server admin. Response: