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

Commit f737368

Browse files
authored
Add admin API for logging in as a user (#8617)
1 parent 3dc1871 commit f737368

File tree

25 files changed

+475
-87
lines changed

25 files changed

+475
-87
lines changed

changelog.d/8617.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add admin API for logging in as a user.

docs/admin_api/user_admin_api.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,41 @@ The following fields are returned in the JSON response body:
424424
- ``next_token``: integer - Indication for pagination. See above.
425425
- ``total`` - integer - Total number of media.
426426

427+
Login as a user
428+
===============
429+
430+
Get an access token that can be used to authenticate as that user. Useful for
431+
when admins wish to do actions on behalf of a user.
432+
433+
The API is::
434+
435+
POST /_synapse/admin/v1/users/<user_id>/login
436+
{}
437+
438+
An optional ``valid_until_ms`` field can be specified in the request body as an
439+
integer timestamp that specifies when the token should expire. By default tokens
440+
do not expire.
441+
442+
A response body like the following is returned:
443+
444+
.. code:: json
445+
446+
{
447+
"access_token": "<opaque_access_token_string>"
448+
}
449+
450+
451+
This API does *not* generate a new device for the user, and so will not appear
452+
their ``/devices`` list, and in general the target user should not be able to
453+
tell they have been logged in as.
454+
455+
To expire the token call the standard ``/logout`` API with the token.
456+
457+
Note: The token will expire if the *admin* user calls ``/logout/all`` from any
458+
of their devices, but the token will *not* expire if the target user does the
459+
same.
460+
461+
427462
User devices
428463
============
429464

synapse/api/auth_blocking.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
# limitations under the License.
1515

1616
import logging
17+
from typing import Optional
1718

1819
from synapse.api.constants import LimitBlockingTypes, UserTypes
1920
from synapse.api.errors import Codes, ResourceLimitError
2021
from synapse.config.server import is_threepid_reserved
22+
from synapse.types import Requester
2123

2224
logger = logging.getLogger(__name__)
2325

@@ -33,24 +35,47 @@ def __init__(self, hs):
3335
self._max_mau_value = hs.config.max_mau_value
3436
self._limit_usage_by_mau = hs.config.limit_usage_by_mau
3537
self._mau_limits_reserved_threepids = hs.config.mau_limits_reserved_threepids
38+
self._server_name = hs.hostname
3639

37-
async def check_auth_blocking(self, user_id=None, threepid=None, user_type=None):
40+
async def check_auth_blocking(
41+
self,
42+
user_id: Optional[str] = None,
43+
threepid: Optional[dict] = None,
44+
user_type: Optional[str] = None,
45+
requester: Optional[Requester] = None,
46+
):
3847
"""Checks if the user should be rejected for some external reason,
3948
such as monthly active user limiting or global disable flag
4049
4150
Args:
42-
user_id(str|None): If present, checks for presence against existing
51+
user_id: If present, checks for presence against existing
4352
MAU cohort
4453
45-
threepid(dict|None): If present, checks for presence against configured
54+
threepid: If present, checks for presence against configured
4655
reserved threepid. Used in cases where the user is trying register
4756
with a MAU blocked server, normally they would be rejected but their
4857
threepid is on the reserved list. user_id and
4958
threepid should never be set at the same time.
5059
51-
user_type(str|None): If present, is used to decide whether to check against
60+
user_type: If present, is used to decide whether to check against
5261
certain blocking reasons like MAU.
62+
63+
requester: If present, and the authenticated entity is a user, checks for
64+
presence against existing MAU cohort. Passing in both a `user_id` and
65+
`requester` is an error.
5366
"""
67+
if requester and user_id:
68+
raise Exception(
69+
"Passed in both 'user_id' and 'requester' to 'check_auth_blocking'"
70+
)
71+
72+
if requester:
73+
if requester.authenticated_entity.startswith("@"):
74+
user_id = requester.authenticated_entity
75+
elif requester.authenticated_entity == self._server_name:
76+
# We never block the server from doing actions on behalf of
77+
# users.
78+
return
5479

5580
# Never fail an auth check for the server notices users or support user
5681
# This can be a problem where event creation is prohibited due to blocking

synapse/handlers/_base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ async def kick_guest_users(self, current_state):
169169
# and having homeservers have their own users leave keeps more
170170
# of that decision-making and control local to the guest-having
171171
# homeserver.
172-
requester = synapse.types.create_requester(target_user, is_guest=True)
172+
requester = synapse.types.create_requester(
173+
target_user, is_guest=True, authenticated_entity=self.server_name
174+
)
173175
handler = self.hs.get_room_member_handler()
174176
await handler.update_membership(
175177
requester,

synapse/handlers/auth.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -698,8 +698,12 @@ def _auth_dict_for_flows(
698698
}
699699

700700
async def get_access_token_for_user_id(
701-
self, user_id: str, device_id: Optional[str], valid_until_ms: Optional[int]
702-
):
701+
self,
702+
user_id: str,
703+
device_id: Optional[str],
704+
valid_until_ms: Optional[int],
705+
puppets_user_id: Optional[str] = None,
706+
) -> str:
703707
"""
704708
Creates a new access token for the user with the given user ID.
705709
@@ -725,13 +729,25 @@ async def get_access_token_for_user_id(
725729
fmt_expiry = time.strftime(
726730
" until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0)
727731
)
728-
logger.info("Logging in user %s on device %s%s", user_id, device_id, fmt_expiry)
732+
733+
if puppets_user_id:
734+
logger.info(
735+
"Logging in user %s as %s%s", user_id, puppets_user_id, fmt_expiry
736+
)
737+
else:
738+
logger.info(
739+
"Logging in user %s on device %s%s", user_id, device_id, fmt_expiry
740+
)
729741

730742
await self.auth.check_auth_blocking(user_id)
731743

732744
access_token = self.macaroon_gen.generate_access_token(user_id)
733745
await self.store.add_access_token_to_user(
734-
user_id, access_token, device_id, valid_until_ms
746+
user_id=user_id,
747+
token=access_token,
748+
device_id=device_id,
749+
valid_until_ms=valid_until_ms,
750+
puppets_user_id=puppets_user_id,
735751
)
736752

737753
# the device *should* have been registered before we got here; however,

synapse/handlers/deactivate_account.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(self, hs: "HomeServer"):
3939
self._room_member_handler = hs.get_room_member_handler()
4040
self._identity_handler = hs.get_identity_handler()
4141
self.user_directory_handler = hs.get_user_directory_handler()
42+
self._server_name = hs.hostname
4243

4344
# Flag that indicates whether the process to part users from rooms is running
4445
self._user_parter_running = False
@@ -152,7 +153,7 @@ async def _reject_pending_invites_for_user(self, user_id: str) -> None:
152153
for room in pending_invites:
153154
try:
154155
await self._room_member_handler.update_membership(
155-
create_requester(user),
156+
create_requester(user, authenticated_entity=self._server_name),
156157
user,
157158
room.room_id,
158159
"leave",
@@ -208,7 +209,7 @@ async def _part_user(self, user_id: str) -> None:
208209
logger.info("User parter parting %r from %r", user_id, room_id)
209210
try:
210211
await self._room_member_handler.update_membership(
211-
create_requester(user),
212+
create_requester(user, authenticated_entity=self._server_name),
212213
user,
213214
room_id,
214215
"leave",

synapse/handlers/message.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ async def create_event(
472472
Returns:
473473
Tuple of created event, Context
474474
"""
475-
await self.auth.check_auth_blocking(requester.user.to_string())
475+
await self.auth.check_auth_blocking(requester=requester)
476476

477477
if event_dict["type"] == EventTypes.Create and event_dict["state_key"] == "":
478478
room_version = event_dict["content"]["room_version"]
@@ -619,7 +619,13 @@ async def assert_accepted_privacy_policy(self, requester: Requester) -> None:
619619
if requester.app_service is not None:
620620
return
621621

622-
user_id = requester.user.to_string()
622+
user_id = requester.authenticated_entity
623+
if not user_id.startswith("@"):
624+
# The authenticated entity might not be a user, e.g. if it's the
625+
# server puppetting the user.
626+
return
627+
628+
user = UserID.from_string(user_id)
623629

624630
# exempt the system notices user
625631
if (
@@ -639,9 +645,7 @@ async def assert_accepted_privacy_policy(self, requester: Requester) -> None:
639645
if u["consent_version"] == self.config.user_consent_version:
640646
return
641647

642-
consent_uri = self._consent_uri_builder.build_user_consent_uri(
643-
requester.user.localpart
644-
)
648+
consent_uri = self._consent_uri_builder.build_user_consent_uri(user.localpart)
645649
msg = self._block_events_without_consent_error % {"consent_uri": consent_uri}
646650
raise ConsentNotGivenError(msg=msg, consent_uri=consent_uri)
647651

@@ -1252,7 +1256,7 @@ async def _send_dummy_event_for_room(self, room_id: str) -> bool:
12521256
for user_id in members:
12531257
if not self.hs.is_mine_id(user_id):
12541258
continue
1255-
requester = create_requester(user_id)
1259+
requester = create_requester(user_id, authenticated_entity=self.server_name)
12561260
try:
12571261
event, context = await self.create_event(
12581262
requester,
@@ -1273,11 +1277,6 @@ async def _send_dummy_event_for_room(self, room_id: str) -> bool:
12731277
requester, event, context, ratelimit=False, ignore_shadow_ban=True,
12741278
)
12751279
return True
1276-
except ConsentNotGivenError:
1277-
logger.info(
1278-
"Failed to send dummy event into room %s for user %s due to "
1279-
"lack of consent. Will try another user" % (room_id, user_id)
1280-
)
12811280
except AuthError:
12821281
logger.info(
12831282
"Failed to send dummy event into room %s for user %s due to "

synapse/handlers/profile.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ async def set_displayname(
206206
# the join event to update the displayname in the rooms.
207207
# This must be done by the target user himself.
208208
if by_admin:
209-
requester = create_requester(target_user)
209+
requester = create_requester(
210+
target_user, authenticated_entity=requester.authenticated_entity,
211+
)
210212

211213
await self.store.set_profile_displayname(
212214
target_user.localpart, displayname_to_set
@@ -286,7 +288,9 @@ async def set_avatar_url(
286288

287289
# Same like set_displayname
288290
if by_admin:
289-
requester = create_requester(target_user)
291+
requester = create_requester(
292+
target_user, authenticated_entity=requester.authenticated_entity
293+
)
290294

291295
await self.store.set_profile_avatar_url(target_user.localpart, new_avatar_url)
292296

synapse/handlers/register.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self, hs):
5252
self.ratelimiter = hs.get_registration_ratelimiter()
5353
self.macaroon_gen = hs.get_macaroon_generator()
5454
self._server_notices_mxid = hs.config.server_notices_mxid
55+
self._server_name = hs.hostname
5556

5657
self.spam_checker = hs.get_spam_checker()
5758

@@ -317,7 +318,8 @@ async def _create_and_join_rooms(self, user_id: str):
317318
requires_join = False
318319
if self.hs.config.registration.auto_join_user_id:
319320
fake_requester = create_requester(
320-
self.hs.config.registration.auto_join_user_id
321+
self.hs.config.registration.auto_join_user_id,
322+
authenticated_entity=self._server_name,
321323
)
322324

323325
# If the room requires an invite, add the user to the list of invites.
@@ -329,7 +331,9 @@ async def _create_and_join_rooms(self, user_id: str):
329331
# being necessary this will occur after the invite was sent.
330332
requires_join = True
331333
else:
332-
fake_requester = create_requester(user_id)
334+
fake_requester = create_requester(
335+
user_id, authenticated_entity=self._server_name
336+
)
333337

334338
# Choose whether to federate the new room.
335339
if not self.hs.config.registration.autocreate_auto_join_rooms_federated:
@@ -362,19 +366,16 @@ async def _create_and_join_rooms(self, user_id: str):
362366
# created it, then ensure the first user joins it.
363367
if requires_join:
364368
await room_member_handler.update_membership(
365-
requester=create_requester(user_id),
369+
requester=create_requester(
370+
user_id, authenticated_entity=self._server_name
371+
),
366372
target=UserID.from_string(user_id),
367373
room_id=info["room_id"],
368374
# Since it was just created, there are no remote hosts.
369375
remote_room_hosts=[],
370376
action="join",
371377
ratelimit=False,
372378
)
373-
374-
except ConsentNotGivenError as e:
375-
# Technically not necessary to pull out this error though
376-
# moving away from bare excepts is a good thing to do.
377-
logger.error("Failed to join new user to %r: %r", r, e)
378379
except Exception as e:
379380
logger.error("Failed to join new user to %r: %r", r, e)
380381

@@ -426,7 +427,8 @@ async def _join_rooms(self, user_id: str):
426427
if requires_invite:
427428
await room_member_handler.update_membership(
428429
requester=create_requester(
429-
self.hs.config.registration.auto_join_user_id
430+
self.hs.config.registration.auto_join_user_id,
431+
authenticated_entity=self._server_name,
430432
),
431433
target=UserID.from_string(user_id),
432434
room_id=room_id,
@@ -437,7 +439,9 @@ async def _join_rooms(self, user_id: str):
437439

438440
# Send the join.
439441
await room_member_handler.update_membership(
440-
requester=create_requester(user_id),
442+
requester=create_requester(
443+
user_id, authenticated_entity=self._server_name
444+
),
441445
target=UserID.from_string(user_id),
442446
room_id=room_id,
443447
remote_room_hosts=remote_room_hosts,

synapse/handlers/room.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ async def create_room(
587587
"""
588588
user_id = requester.user.to_string()
589589

590-
await self.auth.check_auth_blocking(user_id)
590+
await self.auth.check_auth_blocking(requester=requester)
591591

592592
if (
593593
self._server_notices_mxid is not None
@@ -1257,7 +1257,9 @@ async def shutdown_room(
12571257
400, "User must be our own: %s" % (new_room_user_id,)
12581258
)
12591259

1260-
room_creator_requester = create_requester(new_room_user_id)
1260+
room_creator_requester = create_requester(
1261+
new_room_user_id, authenticated_entity=requester_user_id
1262+
)
12611263

12621264
info, stream_id = await self._room_creation_handler.create_room(
12631265
room_creator_requester,
@@ -1297,7 +1299,9 @@ async def shutdown_room(
12971299

12981300
try:
12991301
# Kick users from room
1300-
target_requester = create_requester(user_id)
1302+
target_requester = create_requester(
1303+
user_id, authenticated_entity=requester_user_id
1304+
)
13011305
_, stream_id = await self.room_member_handler.update_membership(
13021306
requester=target_requester,
13031307
target=target_requester.user,

0 commit comments

Comments
 (0)