mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Add admin API for logging in as a user (#8617)
This commit is contained in:
parent
3dc1871219
commit
f737368a26
1
changelog.d/8617.feature
Normal file
1
changelog.d/8617.feature
Normal file
@ -0,0 +1 @@
|
||||
Add admin API for logging in as a user.
|
@ -424,6 +424,41 @@ The following fields are returned in the JSON response body:
|
||||
- ``next_token``: integer - Indication for pagination. See above.
|
||||
- ``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
|
||||
============
|
||||
|
||||
|
@ -14,10 +14,12 @@
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from synapse.api.constants import LimitBlockingTypes, UserTypes
|
||||
from synapse.api.errors import Codes, ResourceLimitError
|
||||
from synapse.config.server import is_threepid_reserved
|
||||
from synapse.types import Requester
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -33,24 +35,47 @@ class AuthBlocking:
|
||||
self._max_mau_value = hs.config.max_mau_value
|
||||
self._limit_usage_by_mau = hs.config.limit_usage_by_mau
|
||||
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,
|
||||
such as monthly active user limiting or global disable flag
|
||||
|
||||
Args:
|
||||
user_id(str|None): If present, checks for presence against existing
|
||||
user_id: If present, checks for presence against existing
|
||||
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
|
||||
with a MAU blocked server, normally they would be rejected but their
|
||||
threepid is on the reserved list. user_id and
|
||||
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.
|
||||
|
||||
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
|
||||
# This can be a problem where event creation is prohibited due to blocking
|
||||
|
@ -169,7 +169,9 @@ class BaseHandler:
|
||||
# and having homeservers have their own users leave keeps more
|
||||
# of that decision-making and control local to the guest-having
|
||||
# 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()
|
||||
await handler.update_membership(
|
||||
requester,
|
||||
|
@ -698,8 +698,12 @@ class AuthHandler(BaseHandler):
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@ -725,13 +729,25 @@ class AuthHandler(BaseHandler):
|
||||
fmt_expiry = time.strftime(
|
||||
" 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)
|
||||
|
||||
access_token = self.macaroon_gen.generate_access_token(user_id)
|
||||
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,
|
||||
|
@ -39,6 +39,7 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
self._identity_handler = hs.get_identity_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
|
||||
self._user_parter_running = False
|
||||
@ -152,7 +153,7 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
for room in pending_invites:
|
||||
try:
|
||||
await self._room_member_handler.update_membership(
|
||||
create_requester(user),
|
||||
create_requester(user, authenticated_entity=self._server_name),
|
||||
user,
|
||||
room.room_id,
|
||||
"leave",
|
||||
@ -208,7 +209,7 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
logger.info("User parter parting %r from %r", user_id, room_id)
|
||||
try:
|
||||
await self._room_member_handler.update_membership(
|
||||
create_requester(user),
|
||||
create_requester(user, authenticated_entity=self._server_name),
|
||||
user,
|
||||
room_id,
|
||||
"leave",
|
||||
|
@ -472,7 +472,7 @@ class EventCreationHandler:
|
||||
Returns:
|
||||
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"] == "":
|
||||
room_version = event_dict["content"]["room_version"]
|
||||
@ -619,7 +619,13 @@ class EventCreationHandler:
|
||||
if requester.app_service is not None:
|
||||
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
|
||||
if (
|
||||
@ -639,9 +645,7 @@ class EventCreationHandler:
|
||||
if u["consent_version"] == self.config.user_consent_version:
|
||||
return
|
||||
|
||||
consent_uri = self._consent_uri_builder.build_user_consent_uri(
|
||||
requester.user.localpart
|
||||
)
|
||||
consent_uri = self._consent_uri_builder.build_user_consent_uri(user.localpart)
|
||||
msg = self._block_events_without_consent_error % {"consent_uri": consent_uri}
|
||||
raise ConsentNotGivenError(msg=msg, consent_uri=consent_uri)
|
||||
|
||||
@ -1252,7 +1256,7 @@ class EventCreationHandler:
|
||||
for user_id in members:
|
||||
if not self.hs.is_mine_id(user_id):
|
||||
continue
|
||||
requester = create_requester(user_id)
|
||||
requester = create_requester(user_id, authenticated_entity=self.server_name)
|
||||
try:
|
||||
event, context = await self.create_event(
|
||||
requester,
|
||||
@ -1273,11 +1277,6 @@ class EventCreationHandler:
|
||||
requester, event, context, ratelimit=False, ignore_shadow_ban=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:
|
||||
logger.info(
|
||||
"Failed to send dummy event into room %s for user %s due to "
|
||||
|
@ -206,7 +206,9 @@ class ProfileHandler(BaseHandler):
|
||||
# the join event to update the displayname in the rooms.
|
||||
# This must be done by the target user himself.
|
||||
if by_admin:
|
||||
requester = create_requester(target_user)
|
||||
requester = create_requester(
|
||||
target_user, authenticated_entity=requester.authenticated_entity,
|
||||
)
|
||||
|
||||
await self.store.set_profile_displayname(
|
||||
target_user.localpart, displayname_to_set
|
||||
@ -286,7 +288,9 @@ class ProfileHandler(BaseHandler):
|
||||
|
||||
# Same like set_displayname
|
||||
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)
|
||||
|
||||
|
@ -52,6 +52,7 @@ class RegistrationHandler(BaseHandler):
|
||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||
self.macaroon_gen = hs.get_macaroon_generator()
|
||||
self._server_notices_mxid = hs.config.server_notices_mxid
|
||||
self._server_name = hs.hostname
|
||||
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
|
||||
@ -317,7 +318,8 @@ class RegistrationHandler(BaseHandler):
|
||||
requires_join = False
|
||||
if self.hs.config.registration.auto_join_user_id:
|
||||
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.
|
||||
@ -329,7 +331,9 @@ class RegistrationHandler(BaseHandler):
|
||||
# being necessary this will occur after the invite was sent.
|
||||
requires_join = True
|
||||
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.
|
||||
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.
|
||||
if requires_join:
|
||||
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),
|
||||
room_id=info["room_id"],
|
||||
# Since it was just created, there are no remote hosts.
|
||||
@ -370,11 +376,6 @@ class RegistrationHandler(BaseHandler):
|
||||
action="join",
|
||||
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:
|
||||
logger.error("Failed to join new user to %r: %r", r, e)
|
||||
|
||||
@ -426,7 +427,8 @@ class RegistrationHandler(BaseHandler):
|
||||
if requires_invite:
|
||||
await room_member_handler.update_membership(
|
||||
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),
|
||||
room_id=room_id,
|
||||
@ -437,7 +439,9 @@ class RegistrationHandler(BaseHandler):
|
||||
|
||||
# Send the join.
|
||||
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),
|
||||
room_id=room_id,
|
||||
remote_room_hosts=remote_room_hosts,
|
||||
|
@ -587,7 +587,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
"""
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
await self.auth.check_auth_blocking(user_id)
|
||||
await self.auth.check_auth_blocking(requester=requester)
|
||||
|
||||
if (
|
||||
self._server_notices_mxid is not None
|
||||
@ -1257,7 +1257,9 @@ class RoomShutdownHandler:
|
||||
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(
|
||||
room_creator_requester,
|
||||
@ -1297,7 +1299,9 @@ class RoomShutdownHandler:
|
||||
|
||||
try:
|
||||
# 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(
|
||||
requester=target_requester,
|
||||
target=target_requester.user,
|
||||
|
@ -965,6 +965,7 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
|
||||
self.distributor = hs.get_distributor()
|
||||
self.distributor.declare("user_left_room")
|
||||
self._server_name = hs.hostname
|
||||
|
||||
async def _is_remote_room_too_complex(
|
||||
self, room_id: str, remote_room_hosts: List[str]
|
||||
@ -1059,7 +1060,9 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
return event_id, stream_id
|
||||
|
||||
# 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(
|
||||
requester=requester, target=user, room_id=room_id, action="leave"
|
||||
)
|
||||
|
@ -31,6 +31,7 @@ from synapse.types import (
|
||||
Collection,
|
||||
JsonDict,
|
||||
MutableStateMap,
|
||||
Requester,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
StreamToken,
|
||||
@ -260,6 +261,7 @@ class SyncHandler:
|
||||
|
||||
async def wait_for_sync_for_user(
|
||||
self,
|
||||
requester: Requester,
|
||||
sync_config: SyncConfig,
|
||||
since_token: Optional[StreamToken] = None,
|
||||
timeout: int = 0,
|
||||
@ -273,7 +275,7 @@ class SyncHandler:
|
||||
# not been exceeded (if not part of the group by this point, almost certain
|
||||
# auth_blocking will occur)
|
||||
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(
|
||||
sync_config.request_key,
|
||||
|
@ -49,6 +49,7 @@ class ModuleApi:
|
||||
self._store = hs.get_datastore()
|
||||
self._auth = hs.get_auth()
|
||||
self._auth_handler = auth_handler
|
||||
self._server_name = hs.hostname
|
||||
|
||||
# We expose these as properties below in order to attach a helpful docstring.
|
||||
self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient
|
||||
@ -336,7 +337,9 @@ class ModuleApi:
|
||||
SynapseError if the event was not allowed.
|
||||
"""
|
||||
# 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
|
||||
(
|
||||
|
@ -61,6 +61,7 @@ from synapse.rest.admin.users import (
|
||||
UserRestServletV2,
|
||||
UsersRestServlet,
|
||||
UsersRestServletV2,
|
||||
UserTokenRestServlet,
|
||||
WhoisRestServlet,
|
||||
)
|
||||
from synapse.types import RoomStreamToken
|
||||
@ -223,6 +224,7 @@ def register_servlets(hs, http_server):
|
||||
UserAdminServlet(hs).register(http_server)
|
||||
UserMediaRestServlet(hs).register(http_server)
|
||||
UserMembershipRestServlet(hs).register(http_server)
|
||||
UserTokenRestServlet(hs).register(http_server)
|
||||
UserRestServletV2(hs).register(http_server)
|
||||
UsersRestServletV2(hs).register(http_server)
|
||||
DeviceRestServlet(hs).register(http_server)
|
||||
|
@ -309,7 +309,9 @@ class JoinRoomAliasServlet(RestServlet):
|
||||
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"
|
||||
room_state = await self.state_handler.get_current_state(room_id)
|
||||
|
@ -16,7 +16,7 @@ import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
from http import HTTPStatus
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse.api.constants import UserTypes
|
||||
from synapse.api.errors import Codes, NotFoundError, SynapseError
|
||||
@ -37,6 +37,9 @@ from synapse.rest.admin._base import (
|
||||
)
|
||||
from synapse.types import JsonDict, UserID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_GET_PUSHERS_ALLOWED_KEYS = {
|
||||
@ -828,3 +831,52 @@ class UserMediaRestServlet(RestServlet):
|
||||
ret["next_token"] = start + len(media)
|
||||
|
||||
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}
|
||||
|
@ -171,6 +171,7 @@ class SyncRestServlet(RestServlet):
|
||||
)
|
||||
with context:
|
||||
sync_result = await self.sync_handler.wait_for_sync_for_user(
|
||||
requester,
|
||||
sync_config,
|
||||
since_token=since_token,
|
||||
timeout=timeout,
|
||||
|
@ -39,6 +39,7 @@ class ServerNoticesManager:
|
||||
self._room_member_handler = hs.get_room_member_handler()
|
||||
self._event_creation_handler = hs.get_event_creation_handler()
|
||||
self._is_mine_id = hs.is_mine_id
|
||||
self._server_name = hs.hostname
|
||||
|
||||
self._notifier = hs.get_notifier()
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@ -145,7 +148,9 @@ class ServerNoticesManager:
|
||||
"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(
|
||||
requester,
|
||||
config={
|
||||
@ -174,7 +179,9 @@ class ServerNoticesManager:
|
||||
user_id: The ID of the user to invite.
|
||||
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
|
||||
# that's the case, there is no need to re-invite them.
|
||||
|
@ -1110,6 +1110,7 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
|
||||
token: str,
|
||||
device_id: Optional[str],
|
||||
valid_until_ms: Optional[int],
|
||||
puppets_user_id: Optional[str] = None,
|
||||
) -> int:
|
||||
"""Adds an access token for the given user.
|
||||
|
||||
@ -1133,6 +1134,7 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
|
||||
"token": token,
|
||||
"device_id": device_id,
|
||||
"valid_until_ms": valid_until_ms,
|
||||
"puppets_user_id": puppets_user_id,
|
||||
},
|
||||
desc="add_access_token_to_user",
|
||||
)
|
||||
|
@ -282,7 +282,11 @@ class AuthTestCase(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
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):
|
||||
|
@ -16,7 +16,7 @@
|
||||
from synapse.api.errors import Codes, ResourceLimitError
|
||||
from synapse.api.filtering import DEFAULT_FILTER_COLLECTION
|
||||
from synapse.handlers.sync import SyncConfig
|
||||
from synapse.types import UserID
|
||||
from synapse.types import UserID, create_requester
|
||||
|
||||
import tests.unittest
|
||||
import tests.utils
|
||||
@ -38,6 +38,7 @@ class SyncTestCase(tests.unittest.HomeserverTestCase):
|
||||
user_id1 = "@user1:test"
|
||||
user_id2 = "@user2:test"
|
||||
sync_config = self._generate_sync_config(user_id1)
|
||||
requester = create_requester(user_id1)
|
||||
|
||||
self.reactor.advance(100) # So we get not 0 time
|
||||
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
|
||||
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
|
||||
self.auth_blocking._hs_disabled = True
|
||||
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.auth_blocking._hs_disabled = False
|
||||
|
||||
sync_config = self._generate_sync_config(user_id2)
|
||||
requester = create_requester(user_id2)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -94,12 +94,13 @@ class ModuleApiTestCase(HomeserverTestCase):
|
||||
self.assertFalse(hasattr(event, "state_key"))
|
||||
self.assertDictEqual(event.content, content)
|
||||
|
||||
expected_requester = create_requester(
|
||||
user_id, authenticated_entity=self.hs.hostname
|
||||
)
|
||||
|
||||
# Check that the event was sent
|
||||
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
|
||||
create_requester(user_id),
|
||||
event_dict,
|
||||
ratelimit=False,
|
||||
ignore_shadow_ban=True,
|
||||
expected_requester, event_dict, ratelimit=False, ignore_shadow_ban=True,
|
||||
)
|
||||
|
||||
# Create and send a state event
|
||||
@ -128,7 +129,7 @@ class ModuleApiTestCase(HomeserverTestCase):
|
||||
|
||||
# Check that the event was sent
|
||||
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
|
||||
create_requester(user_id),
|
||||
expected_requester,
|
||||
{
|
||||
"type": "m.room.power_levels",
|
||||
"content": content,
|
||||
|
@ -24,8 +24,8 @@ from mock import Mock
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import UserTypes
|
||||
from synapse.api.errors import Codes, HttpResponseException, ResourceLimitError
|
||||
from synapse.rest.client.v1 import login, profile, room
|
||||
from synapse.rest.client.v2_alpha import sync
|
||||
from synapse.rest.client.v1 import login, logout, profile, room
|
||||
from synapse.rest.client.v2_alpha import devices, sync
|
||||
|
||||
from tests import unittest
|
||||
from tests.test_utils import make_awaitable
|
||||
@ -1638,3 +1638,244 @@ class UserMediaRestTestCase(unittest.HomeserverTestCase):
|
||||
self.assertIn("last_access_ts", m)
|
||||
self.assertIn("quarantined_by", 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)
|
||||
|
@ -309,36 +309,6 @@ class CleanupExtremDummyEventsTestCase(HomeserverTestCase):
|
||||
)
|
||||
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)
|
||||
def test_expiry_logic(self):
|
||||
"""Simple test to ensure that _expire_rooms_to_exclude_from_dummy_event_insertion()
|
||||
|
@ -169,6 +169,7 @@ class StateTestCase(unittest.TestCase):
|
||||
"get_state_handler",
|
||||
"get_clock",
|
||||
"get_state_resolution_handler",
|
||||
"hostname",
|
||||
]
|
||||
)
|
||||
hs.config = default_config("tesths", True)
|
||||
|
Loading…
Reference in New Issue
Block a user