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

Commit d781a81

Browse files
erikjohnstonara4n
andauthored
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 <[email protected]>
1 parent 5e7d75d commit d781a81

File tree

5 files changed

+294
-3
lines changed

5 files changed

+294
-3
lines changed

changelog.d/8756.feature

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

docs/admin_api/rooms.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* [Parameters](#parameters-1)
99
* [Response](#response)
1010
* [Undoing room shutdowns](#undoing-room-shutdowns)
11+
- [Make Room Admin API](#make-room-admin-api)
1112

1213
# List Room API
1314

@@ -467,6 +468,7 @@ The following fields are returned in the JSON response body:
467468
the old room to the new.
468469
* `new_room_id` - A string representing the room ID of the new room.
469470

471+
470472
## Undoing room shutdowns
471473

472474
*Note*: This guide may be outdated by the time you read it. By nature of room shutdowns being performed at the database level,
@@ -492,4 +494,20 @@ You will have to manually handle, if you so choose, the following:
492494

493495
* Aliases that would have been redirected to the Content Violation room.
494496
* Users that would have been booted from the room (and will have been force-joined to the Content Violation room).
495-
* Removal of the Content Violation room if desired.
497+
* Removal of the Content Violation room if desired.
498+
499+
500+
# Make Room Admin API
501+
502+
Grants another user the highest power available to a local user who is in the room.
503+
If the user is not in the room, and it is not publicly joinable, then invite the user.
504+
505+
By default the server admin (the caller) is granted power, but another user can
506+
optionally be specified, e.g.:
507+
508+
```
509+
POST /_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin
510+
{
511+
"user_id": "@foo:example.com"
512+
}
513+
```

synapse/rest/admin/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
DeleteRoomRestServlet,
3939
JoinRoomAliasServlet,
4040
ListRoomRestServlet,
41+
MakeRoomAdminRestServlet,
4142
RoomMembersRestServlet,
4243
RoomRestServlet,
4344
ShutdownRoomRestServlet,
@@ -228,6 +229,7 @@ def register_servlets(hs, http_server):
228229
EventReportDetailRestServlet(hs).register(http_server)
229230
EventReportsRestServlet(hs).register(http_server)
230231
PushersRestServlet(hs).register(http_server)
232+
MakeRoomAdminRestServlet(hs).register(http_server)
231233

232234

233235
def register_servlets_for_client_rest_resource(hs, http_server):

synapse/rest/admin/rooms.py

+134-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
from http import HTTPStatus
1717
from typing import TYPE_CHECKING, List, Optional, Tuple
1818

19-
from synapse.api.constants import EventTypes, JoinRules
20-
from synapse.api.errors import Codes, NotFoundError, SynapseError
19+
from synapse.api.constants import EventTypes, JoinRules, Membership
20+
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
2121
from synapse.http.servlet import (
2222
RestServlet,
2323
assert_params_in_dict,
@@ -37,6 +37,7 @@
3737
if TYPE_CHECKING:
3838
from synapse.server import HomeServer
3939

40+
4041
logger = logging.getLogger(__name__)
4142

4243

@@ -367,3 +368,134 @@ async def on_POST(
367368
)
368369

369370
return 200, {"room_id": room_id}
371+
372+
373+
class MakeRoomAdminRestServlet(RestServlet):
374+
"""Allows a server admin to get power in a room if a local user has power in
375+
a room. Will also invite the user if they're not in the room and it's a
376+
private room. Can specify another user (rather than the admin user) to be
377+
granted power, e.g.:
378+
379+
POST/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin
380+
{
381+
"user_id": "@foo:example.com"
382+
}
383+
"""
384+
385+
PATTERNS = admin_patterns("/rooms/(?P<room_identifier>[^/]*)/make_room_admin")
386+
387+
def __init__(self, hs: "HomeServer"):
388+
self.hs = hs
389+
self.auth = hs.get_auth()
390+
self.room_member_handler = hs.get_room_member_handler()
391+
self.event_creation_handler = hs.get_event_creation_handler()
392+
self.state_handler = hs.get_state_handler()
393+
self.is_mine_id = hs.is_mine_id
394+
395+
async def on_POST(self, request, room_identifier):
396+
requester = await self.auth.get_user_by_req(request)
397+
await assert_user_is_admin(self.auth, requester.user)
398+
content = parse_json_object_from_request(request, allow_empty_body=True)
399+
400+
# Resolve to a room ID, if necessary.
401+
if RoomID.is_valid(room_identifier):
402+
room_id = room_identifier
403+
elif RoomAlias.is_valid(room_identifier):
404+
room_alias = RoomAlias.from_string(room_identifier)
405+
room_id, _ = await self.room_member_handler.lookup_room_alias(room_alias)
406+
room_id = room_id.to_string()
407+
else:
408+
raise SynapseError(
409+
400, "%s was not legal room ID or room alias" % (room_identifier,)
410+
)
411+
412+
# Which user to grant room admin rights to.
413+
user_to_add = content.get("user_id", requester.user.to_string())
414+
415+
# Figure out which local users currently have power in the room, if any.
416+
room_state = await self.state_handler.get_current_state(room_id)
417+
if not room_state:
418+
raise SynapseError(400, "Server not in room")
419+
420+
create_event = room_state[(EventTypes.Create, "")]
421+
power_levels = room_state.get((EventTypes.PowerLevels, ""))
422+
423+
if power_levels is not None:
424+
# We pick the local user with the highest power.
425+
user_power = power_levels.content.get("users", {})
426+
admin_users = [
427+
user_id for user_id in user_power if self.is_mine_id(user_id)
428+
]
429+
admin_users.sort(key=lambda user: user_power[user])
430+
431+
if not admin_users:
432+
raise SynapseError(400, "No local admin user in room")
433+
434+
admin_user_id = admin_users[-1]
435+
436+
pl_content = power_levels.content
437+
else:
438+
# If there is no power level events then the creator has rights.
439+
pl_content = {}
440+
admin_user_id = create_event.sender
441+
if not self.is_mine_id(admin_user_id):
442+
raise SynapseError(
443+
400, "No local admin user in room",
444+
)
445+
446+
# Grant the user power equal to the room admin by attempting to send an
447+
# updated power level event.
448+
new_pl_content = dict(pl_content)
449+
new_pl_content["users"] = dict(pl_content.get("users", {}))
450+
new_pl_content["users"][user_to_add] = new_pl_content["users"][admin_user_id]
451+
452+
fake_requester = create_requester(
453+
admin_user_id, authenticated_entity=requester.authenticated_entity,
454+
)
455+
456+
try:
457+
await self.event_creation_handler.create_and_send_nonmember_event(
458+
fake_requester,
459+
event_dict={
460+
"content": new_pl_content,
461+
"sender": admin_user_id,
462+
"type": EventTypes.PowerLevels,
463+
"state_key": "",
464+
"room_id": room_id,
465+
},
466+
)
467+
except AuthError:
468+
# The admin user we found turned out not to have enough power.
469+
raise SynapseError(
470+
400, "No local admin user in room with power to update power levels."
471+
)
472+
473+
# Now we check if the user we're granting admin rights to is already in
474+
# the room. If not and it's not a public room we invite them.
475+
member_event = room_state.get((EventTypes.Member, user_to_add))
476+
is_joined = False
477+
if member_event:
478+
is_joined = member_event.content["membership"] in (
479+
Membership.JOIN,
480+
Membership.INVITE,
481+
)
482+
483+
if is_joined:
484+
return 200, {}
485+
486+
join_rules = room_state.get((EventTypes.JoinRules, ""))
487+
is_public = False
488+
if join_rules:
489+
is_public = join_rules.content.get("join_rule") == JoinRules.PUBLIC
490+
491+
if is_public:
492+
return 200, {}
493+
494+
await self.room_member_handler.update_membership(
495+
fake_requester,
496+
target=UserID.from_string(user_to_add),
497+
room_id=room_id,
498+
action=Membership.INVITE,
499+
)
500+
501+
return 200, {}

tests/rest/admin/test_room.py

+138
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from mock import Mock
2121

2222
import synapse.rest.admin
23+
from synapse.api.constants import EventTypes, Membership
2324
from synapse.api.errors import Codes
2425
from synapse.rest.client.v1 import directory, events, login, room
2526

@@ -1432,6 +1433,143 @@ def test_join_private_room_if_owner(self):
14321433
self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0])
14331434

14341435

1436+
class MakeRoomAdminTestCase(unittest.HomeserverTestCase):
1437+
servlets = [
1438+
synapse.rest.admin.register_servlets,
1439+
room.register_servlets,
1440+
login.register_servlets,
1441+
]
1442+
1443+
def prepare(self, reactor, clock, homeserver):
1444+
self.admin_user = self.register_user("admin", "pass", admin=True)
1445+
self.admin_user_tok = self.login("admin", "pass")
1446+
1447+
self.creator = self.register_user("creator", "test")
1448+
self.creator_tok = self.login("creator", "test")
1449+
1450+
self.second_user_id = self.register_user("second", "test")
1451+
self.second_tok = self.login("second", "test")
1452+
1453+
self.public_room_id = self.helper.create_room_as(
1454+
self.creator, tok=self.creator_tok, is_public=True
1455+
)
1456+
self.url = "/_synapse/admin/v1/rooms/{}/make_room_admin".format(
1457+
self.public_room_id
1458+
)
1459+
1460+
def test_public_room(self):
1461+
"""Test that getting admin in a public room works.
1462+
"""
1463+
room_id = self.helper.create_room_as(
1464+
self.creator, tok=self.creator_tok, is_public=True
1465+
)
1466+
1467+
channel = self.make_request(
1468+
"POST",
1469+
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
1470+
content={},
1471+
access_token=self.admin_user_tok,
1472+
)
1473+
1474+
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
1475+
1476+
# Now we test that we can join the room and ban a user.
1477+
self.helper.join(room_id, self.admin_user, tok=self.admin_user_tok)
1478+
self.helper.change_membership(
1479+
room_id,
1480+
self.admin_user,
1481+
"@test:test",
1482+
Membership.BAN,
1483+
tok=self.admin_user_tok,
1484+
)
1485+
1486+
def test_private_room(self):
1487+
"""Test that getting admin in a private room works and we get invited.
1488+
"""
1489+
room_id = self.helper.create_room_as(
1490+
self.creator, tok=self.creator_tok, is_public=False,
1491+
)
1492+
1493+
channel = self.make_request(
1494+
"POST",
1495+
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
1496+
content={},
1497+
access_token=self.admin_user_tok,
1498+
)
1499+
1500+
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
1501+
1502+
# Now we test that we can join the room (we should have received an
1503+
# invite) and can ban a user.
1504+
self.helper.join(room_id, self.admin_user, tok=self.admin_user_tok)
1505+
self.helper.change_membership(
1506+
room_id,
1507+
self.admin_user,
1508+
"@test:test",
1509+
Membership.BAN,
1510+
tok=self.admin_user_tok,
1511+
)
1512+
1513+
def test_other_user(self):
1514+
"""Test that giving admin in a public room works to a non-admin user works.
1515+
"""
1516+
room_id = self.helper.create_room_as(
1517+
self.creator, tok=self.creator_tok, is_public=True
1518+
)
1519+
1520+
channel = self.make_request(
1521+
"POST",
1522+
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
1523+
content={"user_id": self.second_user_id},
1524+
access_token=self.admin_user_tok,
1525+
)
1526+
1527+
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
1528+
1529+
# Now we test that we can join the room and ban a user.
1530+
self.helper.join(room_id, self.second_user_id, tok=self.second_tok)
1531+
self.helper.change_membership(
1532+
room_id,
1533+
self.second_user_id,
1534+
"@test:test",
1535+
Membership.BAN,
1536+
tok=self.second_tok,
1537+
)
1538+
1539+
def test_not_enough_power(self):
1540+
"""Test that we get a sensible error if there are no local room admins.
1541+
"""
1542+
room_id = self.helper.create_room_as(
1543+
self.creator, tok=self.creator_tok, is_public=True
1544+
)
1545+
1546+
# The creator drops admin rights in the room.
1547+
pl = self.helper.get_state(
1548+
room_id, EventTypes.PowerLevels, tok=self.creator_tok
1549+
)
1550+
pl["users"][self.creator] = 0
1551+
self.helper.send_state(
1552+
room_id, EventTypes.PowerLevels, body=pl, tok=self.creator_tok
1553+
)
1554+
1555+
channel = self.make_request(
1556+
"POST",
1557+
"/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id),
1558+
content={},
1559+
access_token=self.admin_user_tok,
1560+
)
1561+
1562+
# We expect this to fail with a 400 as there are no room admins.
1563+
#
1564+
# (Note we assert the error message to ensure that it's not denied for
1565+
# some other reason)
1566+
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
1567+
self.assertEqual(
1568+
channel.json_body["error"],
1569+
"No local admin user in room with power to update power levels.",
1570+
)
1571+
1572+
14351573
PURGE_TABLES = [
14361574
"current_state_events",
14371575
"event_backward_extremities",

0 commit comments

Comments
 (0)