Skip to content

Commit f845b3f

Browse files
committed
Add memberships admin API
1 parent 034c5e6 commit f845b3f

File tree

5 files changed

+129
-3
lines changed

5 files changed

+129
-3
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 `memberships` 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+
UserMembershipsRestServlet,
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+
UserMembershipsRestServlet(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: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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,28 @@ async def on_GET(
10541054
return HTTPStatus.OK, rooms_response
10551055

10561056

1057+
class UserMembershipsRestServlet(RestServlet):
1058+
"""
1059+
Get list of left room ID's for a user.
1060+
"""
1061+
1062+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/memberships$")
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+
memberships = await self.store.get_memberships_for_user(user_id)
1075+
1076+
return HTTPStatus.OK, memberships
1077+
1078+
10571079
class PushersRestServlet(RestServlet):
10581080
"""
10591081
Gets information about all pushers for a specific `user_id`.

synapse/storage/databases/main/roommember.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,27 @@ async def get_rooms_user_currently_banned_from(
746746

747747
return frozenset(room_ids)
748748

749+
async def get_memberships_for_user(self, user_id: str) -> dict[str, str]:
750+
"""Returns a dict of room_id to membership state for a given user.
751+
752+
If a remote user only returns rooms this server is currently
753+
participating in.
754+
"""
755+
756+
rows = cast(
757+
list[tuple[str, str]],
758+
await self.db_pool.simple_select_list(
759+
"current_state_events",
760+
keyvalues={
761+
"type": EventTypes.Member,
762+
"state_key": user_id,
763+
},
764+
retcols=["room_id", "membership"],
765+
desc="get_memberships_for_user",
766+
),
767+
)
768+
return dict(rows)
769+
749770
@cached(max_entries=500000, iterable=True)
750771
async def get_rooms_for_user(self, user_id: str) -> frozenset[str]:
751772
"""Returns a set of room_ids the user is currently joined to.

tests/rest/admin/test_room.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2975,6 +2975,86 @@ 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_memberships(self) -> None:
3002+
"""
3003+
Test user memberships 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+
self.assertEqual(200, channel.code, msg=channel.json_body)
3013+
3014+
other_room_id = self.helper.create_room_as(
3015+
self.admin_user, tok=self.admin_user_tok
3016+
)
3017+
3018+
channel = self.make_request(
3019+
"POST",
3020+
f"/_matrix/client/v3/join/{other_room_id}",
3021+
content={"user_id": self.second_user_id},
3022+
access_token=self.second_tok,
3023+
)
3024+
self.assertEqual(200, channel.code, msg=channel.json_body)
3025+
3026+
channel = self.make_request(
3027+
"GET",
3028+
f"/_synapse/admin/v1/users/{self.second_user_id}/memberships",
3029+
access_token=self.admin_user_tok,
3030+
)
3031+
3032+
self.assertEqual(200, channel.code, msg=channel.json_body)
3033+
self.assertEqual(
3034+
{self.public_room_id: Membership.JOIN, other_room_id: Membership.JOIN},
3035+
channel.json_body,
3036+
)
3037+
3038+
channel = self.make_request(
3039+
"POST",
3040+
f"/_matrix/client/v3/rooms/{other_room_id}/leave",
3041+
content={"user_id": self.second_user_id},
3042+
access_token=self.second_tok,
3043+
)
3044+
self.assertEqual(200, channel.code, msg=channel.json_body)
3045+
3046+
channel = self.make_request(
3047+
"GET",
3048+
f"/_synapse/admin/v1/users/{self.second_user_id}/memberships",
3049+
access_token=self.admin_user_tok,
3050+
)
3051+
3052+
self.assertEqual(200, channel.code, msg=channel.json_body)
3053+
self.assertEqual(
3054+
{self.public_room_id: Membership.JOIN, other_room_id: Membership.LEAVE},
3055+
channel.json_body,
3056+
)
3057+
29783058
def test_context_as_non_admin(self) -> None:
29793059
"""
29803060
Test that, without being admin, one cannot use the context admin API

0 commit comments

Comments
 (0)