Allow server admin to get admin bit in rooms where local user is an admin (#8756)

This adds an admin API that allows a server admin to get power in a room if a local user has power in a room. Will also invite the user if they're not in the room and its a private room. Can specify another user (rather than the admin user) to be granted power.

Co-authored-by: Matthew Hodgson <matthew@matrix.org>
This commit is contained in:
Erik Johnston 2020-12-18 15:37:19 +00:00 committed by GitHub
parent 5e7d75daa2
commit d781a81e69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 294 additions and 3 deletions

1
changelog.d/8756.feature Normal file
View file

@ -0,0 +1 @@
Add admin API that lets server admins get power in rooms in which local users have power.

View file

@ -8,6 +8,7 @@
* [Parameters](#parameters-1) * [Parameters](#parameters-1)
* [Response](#response) * [Response](#response)
* [Undoing room shutdowns](#undoing-room-shutdowns) * [Undoing room shutdowns](#undoing-room-shutdowns)
- [Make Room Admin API](#make-room-admin-api)
# List Room API # List Room API
@ -467,6 +468,7 @@ The following fields are returned in the JSON response body:
the old room to the new. the old room to the new.
* `new_room_id` - A string representing the room ID of the new room. * `new_room_id` - A string representing the room ID of the new room.
## Undoing room shutdowns ## Undoing room shutdowns
*Note*: This guide may be outdated by the time you read it. By nature of room shutdowns being performed at the database level, *Note*: This guide may be outdated by the time you read it. By nature of room shutdowns being performed at the database level,
@ -493,3 +495,19 @@ You will have to manually handle, if you so choose, the following:
* Aliases that would have been redirected to the Content Violation room. * Aliases that would have been redirected to the Content Violation room.
* Users that would have been booted from the room (and will have been force-joined to the Content Violation room). * Users that would have been booted from the room (and will have been force-joined to the Content Violation room).
* Removal of the Content Violation room if desired. * Removal of the Content Violation room if desired.
# Make Room Admin API
Grants another user the highest power available to a local user who is in the room.
If the user is not in the room, and it is not publicly joinable, then invite the user.
By default the server admin (the caller) is granted power, but another user can
optionally be specified, e.g.:
```
POST /_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin
{
"user_id": "@foo:example.com"
}
```

View file

@ -38,6 +38,7 @@ from synapse.rest.admin.rooms import (
DeleteRoomRestServlet, DeleteRoomRestServlet,
JoinRoomAliasServlet, JoinRoomAliasServlet,
ListRoomRestServlet, ListRoomRestServlet,
MakeRoomAdminRestServlet,
RoomMembersRestServlet, RoomMembersRestServlet,
RoomRestServlet, RoomRestServlet,
ShutdownRoomRestServlet, ShutdownRoomRestServlet,
@ -228,6 +229,7 @@ def register_servlets(hs, http_server):
EventReportDetailRestServlet(hs).register(http_server) EventReportDetailRestServlet(hs).register(http_server)
EventReportsRestServlet(hs).register(http_server) EventReportsRestServlet(hs).register(http_server)
PushersRestServlet(hs).register(http_server) PushersRestServlet(hs).register(http_server)
MakeRoomAdminRestServlet(hs).register(http_server)
def register_servlets_for_client_rest_resource(hs, http_server): def register_servlets_for_client_rest_resource(hs, http_server):

View file

@ -16,8 +16,8 @@ import logging
from http import HTTPStatus from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
from synapse.api.constants import EventTypes, JoinRules from synapse.api.constants import EventTypes, JoinRules, Membership
from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
@ -37,6 +37,7 @@ from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester
if TYPE_CHECKING: if TYPE_CHECKING:
from synapse.server import HomeServer from synapse.server import HomeServer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -367,3 +368,134 @@ class JoinRoomAliasServlet(RestServlet):
) )
return 200, {"room_id": room_id} return 200, {"room_id": room_id}
class MakeRoomAdminRestServlet(RestServlet):
"""Allows a server admin to get power in a room if a local user has power in
a room. Will also invite the user if they're not in the room and it's a
private room. Can specify another user (rather than the admin user) to be
granted power, e.g.:
POST/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin
{
"user_id": "@foo:example.com"
}
"""
PATTERNS = admin_patterns("/rooms/(?P<room_identifier>[^/]*)/make_room_admin")
def __init__(self, hs: "HomeServer"):
self.hs = hs
self.auth = hs.get_auth()
self.room_member_handler = hs.get_room_member_handler()
self.event_creation_handler = hs.get_event_creation_handler()
self.state_handler = hs.get_state_handler()
self.is_mine_id = hs.is_mine_id
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, allow_empty_body=True)
# Resolve to a room ID, if necessary.
if RoomID.is_valid(room_identifier):
room_id = room_identifier
elif RoomAlias.is_valid(room_identifier):
room_alias = RoomAlias.from_string(room_identifier)
room_id, _ = await self.room_member_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,)
)
# Which user to grant room admin rights to.
user_to_add = content.get("user_id", requester.user.to_string())
# Figure out which local users currently have power in the room, if any.
room_state = await self.state_handler.get_current_state(room_id)
if not room_state:
raise SynapseError(400, "Server not in room")
create_event = room_state[(EventTypes.Create, "")]
power_levels = room_state.get((EventTypes.PowerLevels, ""))
if power_levels is not None:
# We pick the local user with the highest power.
user_power = power_levels.content.get("users", {})
admin_users = [
user_id for user_id in user_power if self.is_mine_id(user_id)
]
admin_users.sort(key=lambda user: user_power[user])
if not admin_users:
raise SynapseError(400, "No local admin user in room")
admin_user_id = admin_users[-1]
pl_content = power_levels.content
else:
# If there is no power level events then the creator has rights.
pl_content = {}
admin_user_id = create_event.sender
if not self.is_mine_id(admin_user_id):
raise SynapseError(
400, "No local admin user in room",
)
# Grant the user power equal to the room admin by attempting to send an
# updated power level event.
new_pl_content = dict(pl_content)
new_pl_content["users"] = dict(pl_content.get("users", {}))
new_pl_content["users"][user_to_add] = new_pl_content["users"][admin_user_id]
fake_requester = create_requester(
admin_user_id, authenticated_entity=requester.authenticated_entity,
)
try:
await self.event_creation_handler.create_and_send_nonmember_event(
fake_requester,
event_dict={
"content": new_pl_content,
"sender": admin_user_id,
"type": EventTypes.PowerLevels,
"state_key": "",
"room_id": room_id,
},
)
except AuthError:
# The admin user we found turned out not to have enough power.
raise SynapseError(
400, "No local admin user in room with power to update power levels."
)
# Now we check if the user we're granting admin rights to is already in
# the room. If not and it's not a public room we invite them.
member_event = room_state.get((EventTypes.Member, user_to_add))
is_joined = False
if member_event:
is_joined = member_event.content["membership"] in (
Membership.JOIN,
Membership.INVITE,
)
if is_joined:
return 200, {}
join_rules = room_state.get((EventTypes.JoinRules, ""))
is_public = False
if join_rules:
is_public = join_rules.content.get("join_rule") == JoinRules.PUBLIC
if is_public:
return 200, {}
await self.room_member_handler.update_membership(
fake_requester,
target=UserID.from_string(user_to_add),
room_id=room_id,
action=Membership.INVITE,
)
return 200, {}

View file

@ -20,6 +20,7 @@ from typing import List, Optional
from mock import Mock from mock import Mock
import synapse.rest.admin import synapse.rest.admin
from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import Codes from synapse.api.errors import Codes
from synapse.rest.client.v1 import directory, events, login, room from synapse.rest.client.v1 import directory, events, login, room
@ -1432,6 +1433,143 @@ class JoinAliasRoomTestCase(unittest.HomeserverTestCase):
self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0])
class MakeRoomAdminTestCase(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/rooms/{}/make_room_admin".format(
self.public_room_id
)
def test_public_room(self):
"""Test that getting admin in a public room works.
"""
room_id = self.helper.create_room_as(
self.creator, tok=self.creator_tok, is_public=True
)
channel = self.make_request(
"POST",
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
content={},
access_token=self.admin_user_tok,
)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Now we test that we can join the room and ban a user.
self.helper.join(room_id, self.admin_user, tok=self.admin_user_tok)
self.helper.change_membership(
room_id,
self.admin_user,
"@test:test",
Membership.BAN,
tok=self.admin_user_tok,
)
def test_private_room(self):
"""Test that getting admin in a private room works and we get invited.
"""
room_id = self.helper.create_room_as(
self.creator, tok=self.creator_tok, is_public=False,
)
channel = self.make_request(
"POST",
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
content={},
access_token=self.admin_user_tok,
)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Now we test that we can join the room (we should have received an
# invite) and can ban a user.
self.helper.join(room_id, self.admin_user, tok=self.admin_user_tok)
self.helper.change_membership(
room_id,
self.admin_user,
"@test:test",
Membership.BAN,
tok=self.admin_user_tok,
)
def test_other_user(self):
"""Test that giving admin in a public room works to a non-admin user works.
"""
room_id = self.helper.create_room_as(
self.creator, tok=self.creator_tok, is_public=True
)
channel = self.make_request(
"POST",
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
content={"user_id": self.second_user_id},
access_token=self.admin_user_tok,
)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Now we test that we can join the room and ban a user.
self.helper.join(room_id, self.second_user_id, tok=self.second_tok)
self.helper.change_membership(
room_id,
self.second_user_id,
"@test:test",
Membership.BAN,
tok=self.second_tok,
)
def test_not_enough_power(self):
"""Test that we get a sensible error if there are no local room admins.
"""
room_id = self.helper.create_room_as(
self.creator, tok=self.creator_tok, is_public=True
)
# The creator drops admin rights in the room.
pl = self.helper.get_state(
room_id, EventTypes.PowerLevels, tok=self.creator_tok
)
pl["users"][self.creator] = 0
self.helper.send_state(
room_id, EventTypes.PowerLevels, body=pl, tok=self.creator_tok
)
channel = self.make_request(
"POST",
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
content={},
access_token=self.admin_user_tok,
)
# We expect this to fail with a 400 as there are no room admins.
#
# (Note we assert the error message to ensure that it's not denied for
# some other reason)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(
channel.json_body["error"],
"No local admin user in room with power to update power levels.",
)
PURGE_TABLES = [ PURGE_TABLES = [
"current_state_events", "current_state_events",
"event_backward_extremities", "event_backward_extremities",