Add admin API for logging in as a user (#8617)

This commit is contained in:
Erik Johnston 2020-11-17 10:51:25 +00:00 committed by GitHub
parent 3dc1871219
commit f737368a26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 475 additions and 87 deletions

1
changelog.d/8617.feature Normal file
View File

@ -0,0 +1 @@
Add admin API for logging in as a user.

View File

@ -424,6 +424,41 @@ The following fields are returned in the JSON response body:
- ``next_token``: integer - Indication for pagination. See above. - ``next_token``: integer - Indication for pagination. See above.
- ``total`` - integer - Total number of media. - ``total`` - integer - Total number of media.
Login as a user
===============
Get an access token that can be used to authenticate as that user. Useful for
when admins wish to do actions on behalf of a user.
The API is::
POST /_synapse/admin/v1/users/<user_id>/login
{}
An optional ``valid_until_ms`` field can be specified in the request body as an
integer timestamp that specifies when the token should expire. By default tokens
do not expire.
A response body like the following is returned:
.. code:: json
{
"access_token": "<opaque_access_token_string>"
}
This API does *not* generate a new device for the user, and so will not appear
their ``/devices`` list, and in general the target user should not be able to
tell they have been logged in as.
To expire the token call the standard ``/logout`` API with the token.
Note: The token will expire if the *admin* user calls ``/logout/all`` from any
of their devices, but the token will *not* expire if the target user does the
same.
User devices User devices
============ ============

View File

@ -14,10 +14,12 @@
# limitations under the License. # limitations under the License.
import logging import logging
from typing import Optional
from synapse.api.constants import LimitBlockingTypes, UserTypes from synapse.api.constants import LimitBlockingTypes, UserTypes
from synapse.api.errors import Codes, ResourceLimitError from synapse.api.errors import Codes, ResourceLimitError
from synapse.config.server import is_threepid_reserved from synapse.config.server import is_threepid_reserved
from synapse.types import Requester
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,24 +35,47 @@ class AuthBlocking:
self._max_mau_value = hs.config.max_mau_value self._max_mau_value = hs.config.max_mau_value
self._limit_usage_by_mau = hs.config.limit_usage_by_mau self._limit_usage_by_mau = hs.config.limit_usage_by_mau
self._mau_limits_reserved_threepids = hs.config.mau_limits_reserved_threepids self._mau_limits_reserved_threepids = hs.config.mau_limits_reserved_threepids
self._server_name = hs.hostname
async def check_auth_blocking(self, user_id=None, threepid=None, user_type=None): async def check_auth_blocking(
self,
user_id: Optional[str] = None,
threepid: Optional[dict] = None,
user_type: Optional[str] = None,
requester: Optional[Requester] = None,
):
"""Checks if the user should be rejected for some external reason, """Checks if the user should be rejected for some external reason,
such as monthly active user limiting or global disable flag such as monthly active user limiting or global disable flag
Args: Args:
user_id(str|None): If present, checks for presence against existing user_id: If present, checks for presence against existing
MAU cohort MAU cohort
threepid(dict|None): If present, checks for presence against configured threepid: If present, checks for presence against configured
reserved threepid. Used in cases where the user is trying register reserved threepid. Used in cases where the user is trying register
with a MAU blocked server, normally they would be rejected but their with a MAU blocked server, normally they would be rejected but their
threepid is on the reserved list. user_id and threepid is on the reserved list. user_id and
threepid should never be set at the same time. threepid should never be set at the same time.
user_type(str|None): If present, is used to decide whether to check against user_type: If present, is used to decide whether to check against
certain blocking reasons like MAU. certain blocking reasons like MAU.
requester: If present, and the authenticated entity is a user, checks for
presence against existing MAU cohort. Passing in both a `user_id` and
`requester` is an error.
""" """
if requester and user_id:
raise Exception(
"Passed in both 'user_id' and 'requester' to 'check_auth_blocking'"
)
if requester:
if requester.authenticated_entity.startswith("@"):
user_id = requester.authenticated_entity
elif requester.authenticated_entity == self._server_name:
# We never block the server from doing actions on behalf of
# users.
return
# Never fail an auth check for the server notices users or support user # Never fail an auth check for the server notices users or support user
# This can be a problem where event creation is prohibited due to blocking # This can be a problem where event creation is prohibited due to blocking

View File

@ -169,7 +169,9 @@ class BaseHandler:
# and having homeservers have their own users leave keeps more # and having homeservers have their own users leave keeps more
# of that decision-making and control local to the guest-having # of that decision-making and control local to the guest-having
# homeserver. # homeserver.
requester = synapse.types.create_requester(target_user, is_guest=True) requester = synapse.types.create_requester(
target_user, is_guest=True, authenticated_entity=self.server_name
)
handler = self.hs.get_room_member_handler() handler = self.hs.get_room_member_handler()
await handler.update_membership( await handler.update_membership(
requester, requester,

View File

@ -698,8 +698,12 @@ class AuthHandler(BaseHandler):
} }
async def get_access_token_for_user_id( async def get_access_token_for_user_id(
self, user_id: str, device_id: Optional[str], valid_until_ms: Optional[int] self,
): user_id: str,
device_id: Optional[str],
valid_until_ms: Optional[int],
puppets_user_id: Optional[str] = None,
) -> str:
""" """
Creates a new access token for the user with the given user ID. Creates a new access token for the user with the given user ID.
@ -725,13 +729,25 @@ class AuthHandler(BaseHandler):
fmt_expiry = time.strftime( fmt_expiry = time.strftime(
" until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0) " until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0)
) )
logger.info("Logging in user %s on device %s%s", user_id, device_id, fmt_expiry)
if puppets_user_id:
logger.info(
"Logging in user %s as %s%s", user_id, puppets_user_id, fmt_expiry
)
else:
logger.info(
"Logging in user %s on device %s%s", user_id, device_id, fmt_expiry
)
await self.auth.check_auth_blocking(user_id) await self.auth.check_auth_blocking(user_id)
access_token = self.macaroon_gen.generate_access_token(user_id) access_token = self.macaroon_gen.generate_access_token(user_id)
await self.store.add_access_token_to_user( await self.store.add_access_token_to_user(
user_id, access_token, device_id, valid_until_ms user_id=user_id,
token=access_token,
device_id=device_id,
valid_until_ms=valid_until_ms,
puppets_user_id=puppets_user_id,
) )
# the device *should* have been registered before we got here; however, # the device *should* have been registered before we got here; however,

View File

@ -39,6 +39,7 @@ class DeactivateAccountHandler(BaseHandler):
self._room_member_handler = hs.get_room_member_handler() self._room_member_handler = hs.get_room_member_handler()
self._identity_handler = hs.get_identity_handler() self._identity_handler = hs.get_identity_handler()
self.user_directory_handler = hs.get_user_directory_handler() self.user_directory_handler = hs.get_user_directory_handler()
self._server_name = hs.hostname
# Flag that indicates whether the process to part users from rooms is running # Flag that indicates whether the process to part users from rooms is running
self._user_parter_running = False self._user_parter_running = False
@ -152,7 +153,7 @@ class DeactivateAccountHandler(BaseHandler):
for room in pending_invites: for room in pending_invites:
try: try:
await self._room_member_handler.update_membership( await self._room_member_handler.update_membership(
create_requester(user), create_requester(user, authenticated_entity=self._server_name),
user, user,
room.room_id, room.room_id,
"leave", "leave",
@ -208,7 +209,7 @@ class DeactivateAccountHandler(BaseHandler):
logger.info("User parter parting %r from %r", user_id, room_id) logger.info("User parter parting %r from %r", user_id, room_id)
try: try:
await self._room_member_handler.update_membership( await self._room_member_handler.update_membership(
create_requester(user), create_requester(user, authenticated_entity=self._server_name),
user, user,
room_id, room_id,
"leave", "leave",

View File

@ -472,7 +472,7 @@ class EventCreationHandler:
Returns: Returns:
Tuple of created event, Context Tuple of created event, Context
""" """
await self.auth.check_auth_blocking(requester.user.to_string()) await self.auth.check_auth_blocking(requester=requester)
if event_dict["type"] == EventTypes.Create and event_dict["state_key"] == "": if event_dict["type"] == EventTypes.Create and event_dict["state_key"] == "":
room_version = event_dict["content"]["room_version"] room_version = event_dict["content"]["room_version"]
@ -619,7 +619,13 @@ class EventCreationHandler:
if requester.app_service is not None: if requester.app_service is not None:
return return
user_id = requester.user.to_string() user_id = requester.authenticated_entity
if not user_id.startswith("@"):
# The authenticated entity might not be a user, e.g. if it's the
# server puppetting the user.
return
user = UserID.from_string(user_id)
# exempt the system notices user # exempt the system notices user
if ( if (
@ -639,9 +645,7 @@ class EventCreationHandler:
if u["consent_version"] == self.config.user_consent_version: if u["consent_version"] == self.config.user_consent_version:
return return
consent_uri = self._consent_uri_builder.build_user_consent_uri( consent_uri = self._consent_uri_builder.build_user_consent_uri(user.localpart)
requester.user.localpart
)
msg = self._block_events_without_consent_error % {"consent_uri": consent_uri} msg = self._block_events_without_consent_error % {"consent_uri": consent_uri}
raise ConsentNotGivenError(msg=msg, consent_uri=consent_uri) raise ConsentNotGivenError(msg=msg, consent_uri=consent_uri)
@ -1252,7 +1256,7 @@ class EventCreationHandler:
for user_id in members: for user_id in members:
if not self.hs.is_mine_id(user_id): if not self.hs.is_mine_id(user_id):
continue continue
requester = create_requester(user_id) requester = create_requester(user_id, authenticated_entity=self.server_name)
try: try:
event, context = await self.create_event( event, context = await self.create_event(
requester, requester,
@ -1273,11 +1277,6 @@ class EventCreationHandler:
requester, event, context, ratelimit=False, ignore_shadow_ban=True, requester, event, context, ratelimit=False, ignore_shadow_ban=True,
) )
return True return True
except ConsentNotGivenError:
logger.info(
"Failed to send dummy event into room %s for user %s due to "
"lack of consent. Will try another user" % (room_id, user_id)
)
except AuthError: except AuthError:
logger.info( logger.info(
"Failed to send dummy event into room %s for user %s due to " "Failed to send dummy event into room %s for user %s due to "

View File

@ -206,7 +206,9 @@ class ProfileHandler(BaseHandler):
# the join event to update the displayname in the rooms. # the join event to update the displayname in the rooms.
# This must be done by the target user himself. # This must be done by the target user himself.
if by_admin: if by_admin:
requester = create_requester(target_user) requester = create_requester(
target_user, authenticated_entity=requester.authenticated_entity,
)
await self.store.set_profile_displayname( await self.store.set_profile_displayname(
target_user.localpart, displayname_to_set target_user.localpart, displayname_to_set
@ -286,7 +288,9 @@ class ProfileHandler(BaseHandler):
# Same like set_displayname # Same like set_displayname
if by_admin: if by_admin:
requester = create_requester(target_user) requester = create_requester(
target_user, authenticated_entity=requester.authenticated_entity
)
await self.store.set_profile_avatar_url(target_user.localpart, new_avatar_url) await self.store.set_profile_avatar_url(target_user.localpart, new_avatar_url)

View File

@ -52,6 +52,7 @@ class RegistrationHandler(BaseHandler):
self.ratelimiter = hs.get_registration_ratelimiter() self.ratelimiter = hs.get_registration_ratelimiter()
self.macaroon_gen = hs.get_macaroon_generator() self.macaroon_gen = hs.get_macaroon_generator()
self._server_notices_mxid = hs.config.server_notices_mxid self._server_notices_mxid = hs.config.server_notices_mxid
self._server_name = hs.hostname
self.spam_checker = hs.get_spam_checker() self.spam_checker = hs.get_spam_checker()
@ -317,7 +318,8 @@ class RegistrationHandler(BaseHandler):
requires_join = False requires_join = False
if self.hs.config.registration.auto_join_user_id: if self.hs.config.registration.auto_join_user_id:
fake_requester = create_requester( fake_requester = create_requester(
self.hs.config.registration.auto_join_user_id self.hs.config.registration.auto_join_user_id,
authenticated_entity=self._server_name,
) )
# If the room requires an invite, add the user to the list of invites. # If the room requires an invite, add the user to the list of invites.
@ -329,7 +331,9 @@ class RegistrationHandler(BaseHandler):
# being necessary this will occur after the invite was sent. # being necessary this will occur after the invite was sent.
requires_join = True requires_join = True
else: else:
fake_requester = create_requester(user_id) fake_requester = create_requester(
user_id, authenticated_entity=self._server_name
)
# Choose whether to federate the new room. # Choose whether to federate the new room.
if not self.hs.config.registration.autocreate_auto_join_rooms_federated: if not self.hs.config.registration.autocreate_auto_join_rooms_federated:
@ -362,7 +366,9 @@ class RegistrationHandler(BaseHandler):
# created it, then ensure the first user joins it. # created it, then ensure the first user joins it.
if requires_join: if requires_join:
await room_member_handler.update_membership( await room_member_handler.update_membership(
requester=create_requester(user_id), requester=create_requester(
user_id, authenticated_entity=self._server_name
),
target=UserID.from_string(user_id), target=UserID.from_string(user_id),
room_id=info["room_id"], room_id=info["room_id"],
# Since it was just created, there are no remote hosts. # Since it was just created, there are no remote hosts.
@ -370,11 +376,6 @@ class RegistrationHandler(BaseHandler):
action="join", action="join",
ratelimit=False, ratelimit=False,
) )
except ConsentNotGivenError as e:
# Technically not necessary to pull out this error though
# moving away from bare excepts is a good thing to do.
logger.error("Failed to join new user to %r: %r", r, e)
except Exception as e: except Exception as e:
logger.error("Failed to join new user to %r: %r", r, e) logger.error("Failed to join new user to %r: %r", r, e)
@ -426,7 +427,8 @@ class RegistrationHandler(BaseHandler):
if requires_invite: if requires_invite:
await room_member_handler.update_membership( await room_member_handler.update_membership(
requester=create_requester( requester=create_requester(
self.hs.config.registration.auto_join_user_id self.hs.config.registration.auto_join_user_id,
authenticated_entity=self._server_name,
), ),
target=UserID.from_string(user_id), target=UserID.from_string(user_id),
room_id=room_id, room_id=room_id,
@ -437,7 +439,9 @@ class RegistrationHandler(BaseHandler):
# Send the join. # Send the join.
await room_member_handler.update_membership( await room_member_handler.update_membership(
requester=create_requester(user_id), requester=create_requester(
user_id, authenticated_entity=self._server_name
),
target=UserID.from_string(user_id), target=UserID.from_string(user_id),
room_id=room_id, room_id=room_id,
remote_room_hosts=remote_room_hosts, remote_room_hosts=remote_room_hosts,

View File

@ -587,7 +587,7 @@ class RoomCreationHandler(BaseHandler):
""" """
user_id = requester.user.to_string() user_id = requester.user.to_string()
await self.auth.check_auth_blocking(user_id) await self.auth.check_auth_blocking(requester=requester)
if ( if (
self._server_notices_mxid is not None self._server_notices_mxid is not None
@ -1257,7 +1257,9 @@ class RoomShutdownHandler:
400, "User must be our own: %s" % (new_room_user_id,) 400, "User must be our own: %s" % (new_room_user_id,)
) )
room_creator_requester = create_requester(new_room_user_id) room_creator_requester = create_requester(
new_room_user_id, authenticated_entity=requester_user_id
)
info, stream_id = await self._room_creation_handler.create_room( info, stream_id = await self._room_creation_handler.create_room(
room_creator_requester, room_creator_requester,
@ -1297,7 +1299,9 @@ class RoomShutdownHandler:
try: try:
# Kick users from room # Kick users from room
target_requester = create_requester(user_id) target_requester = create_requester(
user_id, authenticated_entity=requester_user_id
)
_, stream_id = await self.room_member_handler.update_membership( _, stream_id = await self.room_member_handler.update_membership(
requester=target_requester, requester=target_requester,
target=target_requester.user, target=target_requester.user,

View File

@ -965,6 +965,7 @@ class RoomMemberMasterHandler(RoomMemberHandler):
self.distributor = hs.get_distributor() self.distributor = hs.get_distributor()
self.distributor.declare("user_left_room") self.distributor.declare("user_left_room")
self._server_name = hs.hostname
async def _is_remote_room_too_complex( async def _is_remote_room_too_complex(
self, room_id: str, remote_room_hosts: List[str] self, room_id: str, remote_room_hosts: List[str]
@ -1059,7 +1060,9 @@ class RoomMemberMasterHandler(RoomMemberHandler):
return event_id, stream_id return event_id, stream_id
# The room is too large. Leave. # The room is too large. Leave.
requester = types.create_requester(user, None, False, False, None) requester = types.create_requester(
user, authenticated_entity=self._server_name
)
await self.update_membership( await self.update_membership(
requester=requester, target=user, room_id=room_id, action="leave" requester=requester, target=user, room_id=room_id, action="leave"
) )

View File

@ -31,6 +31,7 @@ from synapse.types import (
Collection, Collection,
JsonDict, JsonDict,
MutableStateMap, MutableStateMap,
Requester,
RoomStreamToken, RoomStreamToken,
StateMap, StateMap,
StreamToken, StreamToken,
@ -260,6 +261,7 @@ class SyncHandler:
async def wait_for_sync_for_user( async def wait_for_sync_for_user(
self, self,
requester: Requester,
sync_config: SyncConfig, sync_config: SyncConfig,
since_token: Optional[StreamToken] = None, since_token: Optional[StreamToken] = None,
timeout: int = 0, timeout: int = 0,
@ -273,7 +275,7 @@ class SyncHandler:
# not been exceeded (if not part of the group by this point, almost certain # not been exceeded (if not part of the group by this point, almost certain
# auth_blocking will occur) # auth_blocking will occur)
user_id = sync_config.user.to_string() user_id = sync_config.user.to_string()
await self.auth.check_auth_blocking(user_id) await self.auth.check_auth_blocking(requester=requester)
res = await self.response_cache.wrap( res = await self.response_cache.wrap(
sync_config.request_key, sync_config.request_key,

View File

@ -49,6 +49,7 @@ class ModuleApi:
self._store = hs.get_datastore() self._store = hs.get_datastore()
self._auth = hs.get_auth() self._auth = hs.get_auth()
self._auth_handler = auth_handler self._auth_handler = auth_handler
self._server_name = hs.hostname
# We expose these as properties below in order to attach a helpful docstring. # We expose these as properties below in order to attach a helpful docstring.
self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient
@ -336,7 +337,9 @@ class ModuleApi:
SynapseError if the event was not allowed. SynapseError if the event was not allowed.
""" """
# Create a requester object # Create a requester object
requester = create_requester(event_dict["sender"]) requester = create_requester(
event_dict["sender"], authenticated_entity=self._server_name
)
# Create and send the event # Create and send the event
( (

View File

@ -61,6 +61,7 @@ from synapse.rest.admin.users import (
UserRestServletV2, UserRestServletV2,
UsersRestServlet, UsersRestServlet,
UsersRestServletV2, UsersRestServletV2,
UserTokenRestServlet,
WhoisRestServlet, WhoisRestServlet,
) )
from synapse.types import RoomStreamToken from synapse.types import RoomStreamToken
@ -223,6 +224,7 @@ def register_servlets(hs, http_server):
UserAdminServlet(hs).register(http_server) UserAdminServlet(hs).register(http_server)
UserMediaRestServlet(hs).register(http_server) UserMediaRestServlet(hs).register(http_server)
UserMembershipRestServlet(hs).register(http_server) UserMembershipRestServlet(hs).register(http_server)
UserTokenRestServlet(hs).register(http_server)
UserRestServletV2(hs).register(http_server) UserRestServletV2(hs).register(http_server)
UsersRestServletV2(hs).register(http_server) UsersRestServletV2(hs).register(http_server)
DeviceRestServlet(hs).register(http_server) DeviceRestServlet(hs).register(http_server)

View File

@ -309,7 +309,9 @@ class JoinRoomAliasServlet(RestServlet):
400, "%s was not legal room ID or room alias" % (room_identifier,) 400, "%s was not legal room ID or room alias" % (room_identifier,)
) )
fake_requester = create_requester(target_user) fake_requester = create_requester(
target_user, authenticated_entity=requester.authenticated_entity
)
# send invite if room has "JoinRules.INVITE" # send invite if room has "JoinRules.INVITE"
room_state = await self.state_handler.get_current_state(room_id) room_state = await self.state_handler.get_current_state(room_id)

View File

@ -16,7 +16,7 @@ import hashlib
import hmac import hmac
import logging import logging
from http import HTTPStatus from http import HTTPStatus
from typing import Tuple from typing import TYPE_CHECKING, Tuple
from synapse.api.constants import UserTypes from synapse.api.constants import UserTypes
from synapse.api.errors import Codes, NotFoundError, SynapseError from synapse.api.errors import Codes, NotFoundError, SynapseError
@ -37,6 +37,9 @@ from synapse.rest.admin._base import (
) )
from synapse.types import JsonDict, UserID from synapse.types import JsonDict, UserID
if TYPE_CHECKING:
from synapse.server import HomeServer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_GET_PUSHERS_ALLOWED_KEYS = { _GET_PUSHERS_ALLOWED_KEYS = {
@ -828,3 +831,52 @@ class UserMediaRestServlet(RestServlet):
ret["next_token"] = start + len(media) ret["next_token"] = start + len(media)
return 200, ret return 200, ret
class UserTokenRestServlet(RestServlet):
"""An admin API for logging in as a user.
Example:
POST /_synapse/admin/v1/users/@test:example.com/login
{}
200 OK
{
"access_token": "<some_token>"
}
"""
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/login$")
def __init__(self, hs: "HomeServer"):
self.hs = hs
self.store = hs.get_datastore()
self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler()
async def on_POST(self, request, user_id):
requester = await self.auth.get_user_by_req(request)
await assert_user_is_admin(self.auth, requester.user)
auth_user = requester.user
if not self.hs.is_mine_id(user_id):
raise SynapseError(400, "Only local users can be logged in as")
body = parse_json_object_from_request(request, allow_empty_body=True)
valid_until_ms = body.get("valid_until_ms")
if valid_until_ms and not isinstance(valid_until_ms, int):
raise SynapseError(400, "'valid_until_ms' parameter must be an int")
if auth_user.to_string() == user_id:
raise SynapseError(400, "Cannot use admin API to login as self")
token = await self.auth_handler.get_access_token_for_user_id(
user_id=auth_user.to_string(),
device_id=None,
valid_until_ms=valid_until_ms,
puppets_user_id=user_id,
)
return 200, {"access_token": token}

View File

@ -171,6 +171,7 @@ class SyncRestServlet(RestServlet):
) )
with context: with context:
sync_result = await self.sync_handler.wait_for_sync_for_user( sync_result = await self.sync_handler.wait_for_sync_for_user(
requester,
sync_config, sync_config,
since_token=since_token, since_token=since_token,
timeout=timeout, timeout=timeout,

View File

@ -39,6 +39,7 @@ class ServerNoticesManager:
self._room_member_handler = hs.get_room_member_handler() self._room_member_handler = hs.get_room_member_handler()
self._event_creation_handler = hs.get_event_creation_handler() self._event_creation_handler = hs.get_event_creation_handler()
self._is_mine_id = hs.is_mine_id self._is_mine_id = hs.is_mine_id
self._server_name = hs.hostname
self._notifier = hs.get_notifier() self._notifier = hs.get_notifier()
self.server_notices_mxid = self._config.server_notices_mxid self.server_notices_mxid = self._config.server_notices_mxid
@ -72,7 +73,9 @@ class ServerNoticesManager:
await self.maybe_invite_user_to_room(user_id, room_id) await self.maybe_invite_user_to_room(user_id, room_id)
system_mxid = self._config.server_notices_mxid system_mxid = self._config.server_notices_mxid
requester = create_requester(system_mxid) requester = create_requester(
system_mxid, authenticated_entity=self._server_name
)
logger.info("Sending server notice to %s", user_id) logger.info("Sending server notice to %s", user_id)
@ -145,7 +148,9 @@ class ServerNoticesManager:
"avatar_url": self._config.server_notices_mxid_avatar_url, "avatar_url": self._config.server_notices_mxid_avatar_url,
} }
requester = create_requester(self.server_notices_mxid) requester = create_requester(
self.server_notices_mxid, authenticated_entity=self._server_name
)
info, _ = await self._room_creation_handler.create_room( info, _ = await self._room_creation_handler.create_room(
requester, requester,
config={ config={
@ -174,7 +179,9 @@ class ServerNoticesManager:
user_id: The ID of the user to invite. user_id: The ID of the user to invite.
room_id: The ID of the room to invite the user to. room_id: The ID of the room to invite the user to.
""" """
requester = create_requester(self.server_notices_mxid) requester = create_requester(
self.server_notices_mxid, authenticated_entity=self._server_name
)
# Check whether the user has already joined or been invited to this room. If # Check whether the user has already joined or been invited to this room. If
# that's the case, there is no need to re-invite them. # that's the case, there is no need to re-invite them.

View File

@ -1110,6 +1110,7 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
token: str, token: str,
device_id: Optional[str], device_id: Optional[str],
valid_until_ms: Optional[int], valid_until_ms: Optional[int],
puppets_user_id: Optional[str] = None,
) -> int: ) -> int:
"""Adds an access token for the given user. """Adds an access token for the given user.
@ -1133,6 +1134,7 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
"token": token, "token": token,
"device_id": device_id, "device_id": device_id,
"valid_until_ms": valid_until_ms, "valid_until_ms": valid_until_ms,
"puppets_user_id": puppets_user_id,
}, },
desc="add_access_token_to_user", desc="add_access_token_to_user",
) )

View File

@ -282,7 +282,11 @@ class AuthTestCase(unittest.TestCase):
) )
) )
self.store.add_access_token_to_user.assert_called_with( self.store.add_access_token_to_user.assert_called_with(
USER_ID, token, "DEVICE", None user_id=USER_ID,
token=token,
device_id="DEVICE",
valid_until_ms=None,
puppets_user_id=None,
) )
def get_user(tok): def get_user(tok):

View File

@ -16,7 +16,7 @@
from synapse.api.errors import Codes, ResourceLimitError from synapse.api.errors import Codes, ResourceLimitError
from synapse.api.filtering import DEFAULT_FILTER_COLLECTION from synapse.api.filtering import DEFAULT_FILTER_COLLECTION
from synapse.handlers.sync import SyncConfig from synapse.handlers.sync import SyncConfig
from synapse.types import UserID from synapse.types import UserID, create_requester
import tests.unittest import tests.unittest
import tests.utils import tests.utils
@ -38,6 +38,7 @@ class SyncTestCase(tests.unittest.HomeserverTestCase):
user_id1 = "@user1:test" user_id1 = "@user1:test"
user_id2 = "@user2:test" user_id2 = "@user2:test"
sync_config = self._generate_sync_config(user_id1) sync_config = self._generate_sync_config(user_id1)
requester = create_requester(user_id1)
self.reactor.advance(100) # So we get not 0 time self.reactor.advance(100) # So we get not 0 time
self.auth_blocking._limit_usage_by_mau = True self.auth_blocking._limit_usage_by_mau = True
@ -45,21 +46,26 @@ class SyncTestCase(tests.unittest.HomeserverTestCase):
# Check that the happy case does not throw errors # Check that the happy case does not throw errors
self.get_success(self.store.upsert_monthly_active_user(user_id1)) self.get_success(self.store.upsert_monthly_active_user(user_id1))
self.get_success(self.sync_handler.wait_for_sync_for_user(sync_config)) self.get_success(
self.sync_handler.wait_for_sync_for_user(requester, sync_config)
)
# Test that global lock works # Test that global lock works
self.auth_blocking._hs_disabled = True self.auth_blocking._hs_disabled = True
e = self.get_failure( e = self.get_failure(
self.sync_handler.wait_for_sync_for_user(sync_config), ResourceLimitError self.sync_handler.wait_for_sync_for_user(requester, sync_config),
ResourceLimitError,
) )
self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED)
self.auth_blocking._hs_disabled = False self.auth_blocking._hs_disabled = False
sync_config = self._generate_sync_config(user_id2) sync_config = self._generate_sync_config(user_id2)
requester = create_requester(user_id2)
e = self.get_failure( e = self.get_failure(
self.sync_handler.wait_for_sync_for_user(sync_config), ResourceLimitError self.sync_handler.wait_for_sync_for_user(requester, sync_config),
ResourceLimitError,
) )
self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED)

View File

@ -94,12 +94,13 @@ class ModuleApiTestCase(HomeserverTestCase):
self.assertFalse(hasattr(event, "state_key")) self.assertFalse(hasattr(event, "state_key"))
self.assertDictEqual(event.content, content) self.assertDictEqual(event.content, content)
expected_requester = create_requester(
user_id, authenticated_entity=self.hs.hostname
)
# Check that the event was sent # Check that the event was sent
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with( self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
create_requester(user_id), expected_requester, event_dict, ratelimit=False, ignore_shadow_ban=True,
event_dict,
ratelimit=False,
ignore_shadow_ban=True,
) )
# Create and send a state event # Create and send a state event
@ -128,7 +129,7 @@ class ModuleApiTestCase(HomeserverTestCase):
# Check that the event was sent # Check that the event was sent
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with( self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
create_requester(user_id), expected_requester,
{ {
"type": "m.room.power_levels", "type": "m.room.power_levels",
"content": content, "content": content,

View File

@ -24,8 +24,8 @@ from mock import Mock
import synapse.rest.admin import synapse.rest.admin
from synapse.api.constants import UserTypes from synapse.api.constants import UserTypes
from synapse.api.errors import Codes, HttpResponseException, ResourceLimitError from synapse.api.errors import Codes, HttpResponseException, ResourceLimitError
from synapse.rest.client.v1 import login, profile, room from synapse.rest.client.v1 import login, logout, profile, room
from synapse.rest.client.v2_alpha import sync from synapse.rest.client.v2_alpha import devices, sync
from tests import unittest from tests import unittest
from tests.test_utils import make_awaitable from tests.test_utils import make_awaitable
@ -1638,3 +1638,244 @@ class UserMediaRestTestCase(unittest.HomeserverTestCase):
self.assertIn("last_access_ts", m) self.assertIn("last_access_ts", m)
self.assertIn("quarantined_by", m) self.assertIn("quarantined_by", m)
self.assertIn("safe_from_quarantine", m) self.assertIn("safe_from_quarantine", m)
class UserTokenRestTestCase(unittest.HomeserverTestCase):
"""Test for /_synapse/admin/v1/users/<user>/login
"""
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
sync.register_servlets,
room.register_servlets,
devices.register_servlets,
logout.register_servlets,
]
def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")
self.other_user = self.register_user("user", "pass")
self.other_user_tok = self.login("user", "pass")
self.url = "/_synapse/admin/v1/users/%s/login" % urllib.parse.quote(
self.other_user
)
def _get_token(self) -> str:
request, channel = self.make_request(
"POST", self.url, b"{}", access_token=self.admin_user_tok
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
return channel.json_body["access_token"]
def test_no_auth(self):
"""Try to login as a user without authentication.
"""
request, channel = self.make_request("POST", self.url, b"{}")
self.render(request)
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
def test_not_admin(self):
"""Try to login as a user as a non-admin user.
"""
request, channel = self.make_request(
"POST", self.url, b"{}", access_token=self.other_user_tok
)
self.render(request)
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
def test_send_event(self):
"""Test that sending event as a user works.
"""
# Create a room.
room_id = self.helper.create_room_as(self.other_user, tok=self.other_user_tok)
# Login in as the user
puppet_token = self._get_token()
# Test that sending works, and generates the event as the right user.
resp = self.helper.send_event(room_id, "com.example.test", tok=puppet_token)
event_id = resp["event_id"]
event = self.get_success(self.store.get_event(event_id))
self.assertEqual(event.sender, self.other_user)
def test_devices(self):
"""Tests that logging in as a user doesn't create a new device for them.
"""
# Login in as the user
self._get_token()
# Check that we don't see a new device in our devices list
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=self.other_user_tok
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# We should only see the one device (from the login in `prepare`)
self.assertEqual(len(channel.json_body["devices"]), 1)
def test_logout(self):
"""Test that calling `/logout` with the token works.
"""
# Login in as the user
puppet_token = self._get_token()
# Test that we can successfully make a request
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Logout with the puppet token
request, channel = self.make_request(
"POST", "logout", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# The puppet token should no longer work
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
# .. but the real user's tokens should still work
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=self.other_user_tok
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
def test_user_logout_all(self):
"""Tests that the target user calling `/logout/all` does *not* expire
the token.
"""
# Login in as the user
puppet_token = self._get_token()
# Test that we can successfully make a request
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Logout all with the real user token
request, channel = self.make_request(
"POST", "logout/all", b"{}", access_token=self.other_user_tok
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# The puppet token should still work
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# .. but the real user's tokens shouldn't
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=self.other_user_tok
)
self.render(request)
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
def test_admin_logout_all(self):
"""Tests that the admin user calling `/logout/all` does expire the
token.
"""
# Login in as the user
puppet_token = self._get_token()
# Test that we can successfully make a request
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Logout all with the admin user token
request, channel = self.make_request(
"POST", "logout/all", b"{}", access_token=self.admin_user_tok
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# The puppet token should no longer work
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=puppet_token
)
self.render(request)
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
# .. but the real user's tokens should still work
request, channel = self.make_request(
"GET", "devices", b"{}", access_token=self.other_user_tok
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
@unittest.override_config(
{
"public_baseurl": "https://example.org/",
"user_consent": {
"version": "1.0",
"policy_name": "My Cool Privacy Policy",
"template_dir": "/",
"require_at_registration": True,
"block_events_error": "You should accept the policy",
},
"form_secret": "123secret",
}
)
def test_consent(self):
"""Test that sending a message is not subject to the privacy policies.
"""
# Have the admin user accept the terms.
self.get_success(self.store.user_set_consent_version(self.admin_user, "1.0"))
# First, cheekily accept the terms and create a room
self.get_success(self.store.user_set_consent_version(self.other_user, "1.0"))
room_id = self.helper.create_room_as(self.other_user, tok=self.other_user_tok)
self.helper.send_event(room_id, "com.example.test", tok=self.other_user_tok)
# Now unaccept it and check that we can't send an event
self.get_success(self.store.user_set_consent_version(self.other_user, "0.0"))
self.helper.send_event(
room_id, "com.example.test", tok=self.other_user_tok, expect_code=403
)
# Login in as the user
puppet_token = self._get_token()
# Sending an event on their behalf should work fine
self.helper.send_event(room_id, "com.example.test", tok=puppet_token)
@override_config(
{"limit_usage_by_mau": True, "max_mau_value": 1, "mau_trial_days": 0}
)
def test_mau_limit(self):
# Create a room as the admin user. This will bump the monthly active users to 1.
room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
# Trying to join as the other user should fail due to reaching MAU limit.
self.helper.join(
room_id, user=self.other_user, tok=self.other_user_tok, expect_code=403
)
# Logging in as the other user and joining a room should work, even
# though the MAU limit would stop the user doing so.
puppet_token = self._get_token()
self.helper.join(room_id, user=self.other_user, tok=puppet_token)

View File

@ -309,36 +309,6 @@ class CleanupExtremDummyEventsTestCase(HomeserverTestCase):
) )
self.assertTrue(len(latest_event_ids) < 10, len(latest_event_ids)) self.assertTrue(len(latest_event_ids) < 10, len(latest_event_ids))
@patch("synapse.handlers.message._DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY", new=0)
def test_send_dummy_event_without_consent(self):
self._create_extremity_rich_graph()
self._enable_consent_checking()
# Pump the reactor repeatedly so that the background updates have a
# chance to run. Attempt to add dummy event with user that has not consented
# Check that dummy event send fails.
self.pump(10 * 60)
latest_event_ids = self.get_success(
self.store.get_latest_event_ids_in_room(self.room_id)
)
self.assertTrue(len(latest_event_ids) == self.EXTREMITIES_COUNT)
# Create new user, and add consent
user2 = self.register_user("user2", "password")
token2 = self.login("user2", "password")
self.get_success(
self.store.user_set_consent_version(user2, self.CONSENT_VERSION)
)
self.helper.join(self.room_id, user2, tok=token2)
# Background updates should now cause a dummy event to be added to the graph
self.pump(10 * 60)
latest_event_ids = self.get_success(
self.store.get_latest_event_ids_in_room(self.room_id)
)
self.assertTrue(len(latest_event_ids) < 10, len(latest_event_ids))
@patch("synapse.handlers.message._DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY", new=250) @patch("synapse.handlers.message._DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY", new=250)
def test_expiry_logic(self): def test_expiry_logic(self):
"""Simple test to ensure that _expire_rooms_to_exclude_from_dummy_event_insertion() """Simple test to ensure that _expire_rooms_to_exclude_from_dummy_event_insertion()

View File

@ -169,6 +169,7 @@ class StateTestCase(unittest.TestCase):
"get_state_handler", "get_state_handler",
"get_clock", "get_clock",
"get_state_resolution_handler", "get_state_resolution_handler",
"hostname",
] ]
) )
hs.config = default_config("tesths", True) hs.config = default_config("tesths", True)