Admin API to join users to a room. (#7051)
parent
8327eb9280
commit
fb69690761
|
@ -0,0 +1 @@
|
||||||
|
Admin API `POST /_synapse/admin/v1/join/<roomIdOrAlias>` to join users to a room like `auto_join_rooms` for creation of users.
|
|
@ -0,0 +1,34 @@
|
||||||
|
# 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 `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`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /_synapse/admin/v1/join/<room_id_or_alias>
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "@user:server.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including an `access_token` of a server admin.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"room_id": "!636q39766251:server.com"
|
||||||
|
}
|
||||||
|
```
|
|
@ -29,7 +29,11 @@ from synapse.rest.admin._base import (
|
||||||
from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
|
from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
|
||||||
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
|
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.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.server_notice_servlet import SendServerNoticeServlet
|
||||||
from synapse.rest.admin.users import (
|
from synapse.rest.admin.users import (
|
||||||
AccountValidityRenewServlet,
|
AccountValidityRenewServlet,
|
||||||
|
@ -189,6 +193,7 @@ def register_servlets(hs, http_server):
|
||||||
"""
|
"""
|
||||||
register_servlets_for_client_rest_resource(hs, http_server)
|
register_servlets_for_client_rest_resource(hs, http_server)
|
||||||
ListRoomRestServlet(hs).register(http_server)
|
ListRoomRestServlet(hs).register(http_server)
|
||||||
|
JoinRoomAliasServlet(hs).register(http_server)
|
||||||
PurgeRoomServlet(hs).register(http_server)
|
PurgeRoomServlet(hs).register(http_server)
|
||||||
SendServerNoticeServlet(hs).register(http_server)
|
SendServerNoticeServlet(hs).register(http_server)
|
||||||
VersionServlet(hs).register(http_server)
|
VersionServlet(hs).register(http_server)
|
||||||
|
|
|
@ -13,9 +13,10 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
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, SynapseError
|
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
RestServlet,
|
RestServlet,
|
||||||
assert_params_in_dict,
|
assert_params_in_dict,
|
||||||
|
@ -29,7 +30,7 @@ from synapse.rest.admin._base import (
|
||||||
historical_admin_path_patterns,
|
historical_admin_path_patterns,
|
||||||
)
|
)
|
||||||
from synapse.storage.data_stores.main.room import RoomSortOrder
|
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
|
from synapse.util.async_helpers import maybe_awaitable
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -237,3 +238,75 @@ class ListRoomRestServlet(RestServlet):
|
||||||
response["prev_batch"] = 0
|
response["prev_batch"] = 0
|
||||||
|
|
||||||
return 200, response
|
return 200, response
|
||||||
|
|
||||||
|
|
||||||
|
class JoinRoomAliasServlet(RestServlet):
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/join/(?P<room_identifier>[^/]*)")
|
||||||
|
|
||||||
|
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
|
||||||
|
self.state_handler = hs.get_state_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)
|
||||||
|
|
||||||
|
content = parse_json_object_from_request(request)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
room_id=room_id,
|
||||||
|
action="join",
|
||||||
|
remote_room_hosts=remote_room_hosts,
|
||||||
|
ratelimit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 200, {"room_id": room_id}
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
# -*- 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.api.errors import Codes
|
||||||
|
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.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):
|
||||||
|
"""
|
||||||
|
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(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
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(Codes.MISSING_PARAM, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
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(Codes.NOT_FOUND, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
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_public_room(self):
|
||||||
|
"""
|
||||||
|
Test joining a local user to a public room with "JoinRules.PUBLIC"
|
||||||
|
"""
|
||||||
|
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.public_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(self.public_room_id, channel.json_body["joined_rooms"][0])
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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_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
|
||||||
|
)
|
||||||
|
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])
|
Loading…
Reference in New Issue