Skip to content

Commit b5263c3

Browse files
committed
Add left_rooms admin API
1 parent 034c5e6 commit b5263c3

File tree

5 files changed

+90
-7
lines changed

5 files changed

+90
-7
lines changed

changelog.d/19260.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `left_rooms` endpoint to the admin API. This is useful for forensics and T&S purpose.

synapse/rest/admin/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@
114114
UserByThreePid,
115115
UserInvitesCount,
116116
UserJoinedRoomCount,
117-
UserMembershipRestServlet,
117+
UserJoinedRoomsRestServlet,
118+
UserLeftRoomsRestServlet,
118119
UserRegisterServlet,
119120
UserReplaceMasterCrossSigningKeyRestServlet,
120121
UserRestServletV2,
@@ -297,7 +298,8 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
297298
VersionServlet(hs).register(http_server)
298299
if not auth_delegated:
299300
UserAdminServlet(hs).register(http_server)
300-
UserMembershipRestServlet(hs).register(http_server)
301+
UserJoinedRoomsRestServlet(hs).register(http_server)
302+
UserLeftRoomsRestServlet(hs).register(http_server)
301303
if not auth_delegated:
302304
UserTokenRestServlet(hs).register(http_server)
303305
UserRestServletV2(hs).register(http_server)

synapse/rest/admin/users.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import attr
2929
from pydantic import StrictBool, StrictInt, StrictStr
3030

31-
from synapse.api.constants import Direction
31+
from synapse.api.constants import Direction, Membership
3232
from synapse.api.errors import Codes, NotFoundError, SynapseError
3333
from synapse.http.servlet import (
3434
RestServlet,
@@ -1031,7 +1031,7 @@ async def on_PUT(
10311031
return HTTPStatus.OK, {}
10321032

10331033

1034-
class UserMembershipRestServlet(RestServlet):
1034+
class UserJoinedRoomsRestServlet(RestServlet):
10351035
"""
10361036
Get list of joined room ID's for a user.
10371037
"""
@@ -1054,6 +1054,29 @@ async def on_GET(
10541054
return HTTPStatus.OK, rooms_response
10551055

10561056

1057+
class UserLeftRoomsRestServlet(RestServlet):
1058+
"""
1059+
Get list of left room ID's for a user.
1060+
"""
1061+
1062+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/left_rooms$")
1063+
1064+
def __init__(self, hs: "HomeServer"):
1065+
self.is_mine = hs.is_mine
1066+
self.auth = hs.get_auth()
1067+
self.store = hs.get_datastores().main
1068+
1069+
async def on_GET(
1070+
self, request: SynapseRequest, user_id: str
1071+
) -> tuple[int, JsonDict]:
1072+
await assert_requester_is_admin(self.auth, request)
1073+
1074+
room_ids = await self.store.get_rooms_for_user(user_id, Membership.LEAVE)
1075+
rooms_response = {"left_rooms": list(room_ids), "total": len(room_ids)}
1076+
1077+
return HTTPStatus.OK, rooms_response
1078+
1079+
10571080
class PushersRestServlet(RestServlet):
10581081
"""
10591082
Gets information about all pushers for a specific `user_id`.

synapse/storage/databases/main/roommember.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,8 +746,10 @@ async def get_rooms_user_currently_banned_from(
746746

747747
return frozenset(room_ids)
748748

749-
@cached(max_entries=500000, iterable=True)
750-
async def get_rooms_for_user(self, user_id: str) -> frozenset[str]:
749+
@cached(num_args=2, max_entries=500000, iterable=True, tree=True)
750+
async def get_rooms_for_user(
751+
self, user_id: str, current_membership: str = Membership.JOIN
752+
) -> frozenset[str]:
751753
"""Returns a set of room_ids the user is currently joined to.
752754
753755
If a remote user only returns rooms this server is currently
@@ -758,7 +760,7 @@ async def get_rooms_for_user(self, user_id: str) -> frozenset[str]:
758760
table="current_state_events",
759761
keyvalues={
760762
"type": EventTypes.Member,
761-
"membership": Membership.JOIN,
763+
"membership": current_membership,
762764
"state_key": user_id,
763765
},
764766
retcol="room_id",

tests/rest/admin/test_room.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2975,6 +2975,61 @@ def test_join_private_room_if_owner(self) -> None:
29752975
self.assertEqual(200, channel.code, msg=channel.json_body)
29762976
self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0])
29772977

2978+
def test_joined_rooms(self) -> None:
2979+
"""
2980+
Test joined_rooms admin endpoint.
2981+
"""
2982+
2983+
channel = self.make_request(
2984+
"POST",
2985+
f"/_matrix/client/v3/join/{self.public_room_id}",
2986+
content={"user_id": self.second_user_id},
2987+
access_token=self.second_tok,
2988+
)
2989+
2990+
self.assertEqual(200, channel.code, msg=channel.json_body)
2991+
self.assertEqual(self.public_room_id, channel.json_body["room_id"])
2992+
2993+
channel = self.make_request(
2994+
"GET",
2995+
f"/_synapse/admin/v1/users/{self.second_user_id}/joined_rooms",
2996+
access_token=self.admin_user_tok,
2997+
)
2998+
self.assertEqual(200, channel.code, msg=channel.json_body)
2999+
self.assertEqual(self.public_room_id, channel.json_body["joined_rooms"][0])
3000+
3001+
def test_left_rooms(self) -> None:
3002+
"""
3003+
Test left_rooms admin endpoint.
3004+
"""
3005+
3006+
channel = self.make_request(
3007+
"POST",
3008+
f"/_matrix/client/v3/join/{self.public_room_id}",
3009+
content={"user_id": self.second_user_id},
3010+
access_token=self.second_tok,
3011+
)
3012+
3013+
self.assertEqual(200, channel.code, msg=channel.json_body)
3014+
self.assertEqual(self.public_room_id, channel.json_body["room_id"])
3015+
3016+
channel = self.make_request(
3017+
"POST",
3018+
f"/_matrix/client/v3/rooms/{self.public_room_id}/leave",
3019+
content={"user_id": self.second_user_id},
3020+
access_token=self.second_tok,
3021+
)
3022+
3023+
self.assertEqual(200, channel.code, msg=channel.json_body)
3024+
3025+
channel = self.make_request(
3026+
"GET",
3027+
f"/_synapse/admin/v1/users/{self.second_user_id}/left_rooms",
3028+
access_token=self.admin_user_tok,
3029+
)
3030+
self.assertEqual(200, channel.code, msg=channel.json_body)
3031+
self.assertEqual(self.public_room_id, channel.json_body["left_rooms"][0])
3032+
29783033
def test_context_as_non_admin(self) -> None:
29793034
"""
29803035
Test that, without being admin, one cannot use the context admin API

0 commit comments

Comments
 (0)