mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Add experimental support for MSC3202: allowing application services to masquerade as specific devices. (#11538)
This commit is contained in:
parent
ecfcd9bbbe
commit
17886d2603
1
changelog.d/11538.feature
Normal file
1
changelog.d/11538.feature
Normal file
@ -0,0 +1 @@
|
||||
Add experimental support for MSC3202: allowing application services to masquerade as specific devices.
|
@ -155,7 +155,11 @@ class Auth:
|
||||
|
||||
access_token = self.get_access_token_from_request(request)
|
||||
|
||||
user_id, app_service = await self._get_appservice_user_id(request)
|
||||
(
|
||||
user_id,
|
||||
device_id,
|
||||
app_service,
|
||||
) = await self._get_appservice_user_id_and_device_id(request)
|
||||
if user_id and app_service:
|
||||
if ip_addr and self._track_appservice_user_ips:
|
||||
await self.store.insert_client_ip(
|
||||
@ -163,16 +167,22 @@ class Auth:
|
||||
access_token=access_token,
|
||||
ip=ip_addr,
|
||||
user_agent=user_agent,
|
||||
device_id="dummy-device", # stubbed
|
||||
device_id="dummy-device"
|
||||
if device_id is None
|
||||
else device_id, # stubbed
|
||||
)
|
||||
|
||||
requester = create_requester(user_id, app_service=app_service)
|
||||
requester = create_requester(
|
||||
user_id, app_service=app_service, device_id=device_id
|
||||
)
|
||||
|
||||
request.requester = user_id
|
||||
if user_id in self._force_tracing_for_users:
|
||||
opentracing.force_tracing()
|
||||
opentracing.set_tag("authenticated_entity", user_id)
|
||||
opentracing.set_tag("user_id", user_id)
|
||||
if device_id is not None:
|
||||
opentracing.set_tag("device_id", device_id)
|
||||
opentracing.set_tag("appservice_id", app_service.id)
|
||||
|
||||
return requester
|
||||
@ -274,33 +284,81 @@ class Auth:
|
||||
403, "Application service has not registered this user (%s)" % user_id
|
||||
)
|
||||
|
||||
async def _get_appservice_user_id(
|
||||
async def _get_appservice_user_id_and_device_id(
|
||||
self, request: Request
|
||||
) -> Tuple[Optional[str], Optional[ApplicationService]]:
|
||||
) -> Tuple[Optional[str], Optional[str], Optional[ApplicationService]]:
|
||||
"""
|
||||
Given a request, reads the request parameters to determine:
|
||||
- whether it's an application service that's making this request
|
||||
- what user the application service should be treated as controlling
|
||||
(the user_id URI parameter allows an application service to masquerade
|
||||
any applicable user in its namespace)
|
||||
- what device the application service should be treated as controlling
|
||||
(the device_id[^1] URI parameter allows an application service to masquerade
|
||||
as any device that exists for the relevant user)
|
||||
|
||||
[^1] Unstable and provided by MSC3202.
|
||||
Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
|
||||
|
||||
Returns:
|
||||
3-tuple of
|
||||
(user ID?, device ID?, application service?)
|
||||
|
||||
Postconditions:
|
||||
- If an application service is returned, so is a user ID
|
||||
- A user ID is never returned without an application service
|
||||
- A device ID is never returned without a user ID or an application service
|
||||
- The returned application service, if present, is permitted to control the
|
||||
returned user ID.
|
||||
- The returned device ID, if present, has been checked to be a valid device ID
|
||||
for the returned user ID.
|
||||
"""
|
||||
DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
|
||||
|
||||
app_service = self.store.get_app_service_by_token(
|
||||
self.get_access_token_from_request(request)
|
||||
)
|
||||
if app_service is None:
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
if app_service.ip_range_whitelist:
|
||||
ip_address = IPAddress(request.getClientIP())
|
||||
if ip_address not in app_service.ip_range_whitelist:
|
||||
return None, None
|
||||
return None, None, None
|
||||
|
||||
# This will always be set by the time Twisted calls us.
|
||||
assert request.args is not None
|
||||
|
||||
if b"user_id" not in request.args:
|
||||
return app_service.sender, app_service
|
||||
if b"user_id" in request.args:
|
||||
effective_user_id = request.args[b"user_id"][0].decode("utf8")
|
||||
await self.validate_appservice_can_control_user_id(
|
||||
app_service, effective_user_id
|
||||
)
|
||||
else:
|
||||
effective_user_id = app_service.sender
|
||||
|
||||
user_id = request.args[b"user_id"][0].decode("utf8")
|
||||
await self.validate_appservice_can_control_user_id(app_service, user_id)
|
||||
effective_device_id: Optional[str] = None
|
||||
|
||||
if app_service.sender == user_id:
|
||||
return app_service.sender, app_service
|
||||
if (
|
||||
self.hs.config.experimental.msc3202_device_masquerading_enabled
|
||||
and DEVICE_ID_ARG_NAME in request.args
|
||||
):
|
||||
effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
|
||||
# We only just set this so it can't be None!
|
||||
assert effective_device_id is not None
|
||||
device_opt = await self.store.get_device(
|
||||
effective_user_id, effective_device_id
|
||||
)
|
||||
if device_opt is None:
|
||||
# For now, use 400 M_EXCLUSIVE if the device doesn't exist.
|
||||
# This is an open thread of discussion on MSC3202 as of 2021-12-09.
|
||||
raise AuthError(
|
||||
400,
|
||||
f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
|
||||
Codes.EXCLUSIVE,
|
||||
)
|
||||
|
||||
return user_id, app_service
|
||||
return effective_user_id, effective_device_id, app_service
|
||||
|
||||
async def get_user_by_access_token(
|
||||
self,
|
||||
|
@ -49,3 +49,8 @@ class ExperimentalConfig(Config):
|
||||
|
||||
# MSC3030 (Jump to date API endpoint)
|
||||
self.msc3030_enabled: bool = experimental.get("msc3030_enabled", False)
|
||||
|
||||
# The portion of MSC3202 which is related to device masquerading.
|
||||
self.msc3202_device_masquerading_enabled: bool = experimental.get(
|
||||
"msc3202_device_masquerading", False
|
||||
)
|
||||
|
@ -128,6 +128,26 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
async def get_device_opt(
|
||||
self, user_id: str, device_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Retrieve a device. Only returns devices that are not marked as
|
||||
hidden.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the user which owns the device
|
||||
device_id: The ID of the device to retrieve
|
||||
Returns:
|
||||
A dict containing the device information, or None if the device does not exist.
|
||||
"""
|
||||
return await self.db_pool.simple_select_one(
|
||||
table="devices",
|
||||
keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False},
|
||||
retcols=("user_id", "device_id", "display_name"),
|
||||
desc="get_device",
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
async def get_devices_by_user(self, user_id: str) -> Dict[str, Dict[str, str]]:
|
||||
"""Retrieve all of a user's registered devices. Only returns devices
|
||||
that are not marked as hidden.
|
||||
|
@ -31,6 +31,7 @@ from synapse.types import Requester
|
||||
|
||||
from tests import unittest
|
||||
from tests.test_utils import simple_async_mock
|
||||
from tests.unittest import override_config
|
||||
from tests.utils import mock_getRawHeaders
|
||||
|
||||
|
||||
@ -210,6 +211,69 @@ class AuthTestCase(unittest.HomeserverTestCase):
|
||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
||||
self.get_failure(self.auth.get_user_by_req(request), AuthError)
|
||||
|
||||
@override_config({"experimental_features": {"msc3202_device_masquerading": True}})
|
||||
def test_get_user_by_req_appservice_valid_token_valid_device_id(self):
|
||||
"""
|
||||
Tests that when an application service passes the device_id URL parameter
|
||||
with the ID of a valid device for the user in question,
|
||||
the requester instance tracks that device ID.
|
||||
"""
|
||||
masquerading_user_id = b"@doppelganger:matrix.org"
|
||||
masquerading_device_id = b"DOPPELDEVICE"
|
||||
app_service = Mock(
|
||||
token="foobar", url="a_url", sender=self.test_user, ip_range_whitelist=None
|
||||
)
|
||||
app_service.is_interested_in_user = Mock(return_value=True)
|
||||
self.store.get_app_service_by_token = Mock(return_value=app_service)
|
||||
# This just needs to return a truth-y value.
|
||||
self.store.get_user_by_id = simple_async_mock({"is_guest": False})
|
||||
self.store.get_user_by_access_token = simple_async_mock(None)
|
||||
# This also needs to just return a truth-y value
|
||||
self.store.get_device = simple_async_mock({"hidden": False})
|
||||
|
||||
request = Mock(args={})
|
||||
request.getClientIP.return_value = "127.0.0.1"
|
||||
request.args[b"access_token"] = [self.test_token]
|
||||
request.args[b"user_id"] = [masquerading_user_id]
|
||||
request.args[b"org.matrix.msc3202.device_id"] = [masquerading_device_id]
|
||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
||||
requester = self.get_success(self.auth.get_user_by_req(request))
|
||||
self.assertEquals(
|
||||
requester.user.to_string(), masquerading_user_id.decode("utf8")
|
||||
)
|
||||
self.assertEquals(requester.device_id, masquerading_device_id.decode("utf8"))
|
||||
|
||||
@override_config({"experimental_features": {"msc3202_device_masquerading": True}})
|
||||
def test_get_user_by_req_appservice_valid_token_invalid_device_id(self):
|
||||
"""
|
||||
Tests that when an application service passes the device_id URL parameter
|
||||
with an ID that is not a valid device ID for the user in question,
|
||||
the request fails with the appropriate error code.
|
||||
"""
|
||||
masquerading_user_id = b"@doppelganger:matrix.org"
|
||||
masquerading_device_id = b"NOT_A_REAL_DEVICE_ID"
|
||||
app_service = Mock(
|
||||
token="foobar", url="a_url", sender=self.test_user, ip_range_whitelist=None
|
||||
)
|
||||
app_service.is_interested_in_user = Mock(return_value=True)
|
||||
self.store.get_app_service_by_token = Mock(return_value=app_service)
|
||||
# This just needs to return a truth-y value.
|
||||
self.store.get_user_by_id = simple_async_mock({"is_guest": False})
|
||||
self.store.get_user_by_access_token = simple_async_mock(None)
|
||||
# This also needs to just return a falsey value
|
||||
self.store.get_device = simple_async_mock(None)
|
||||
|
||||
request = Mock(args={})
|
||||
request.getClientIP.return_value = "127.0.0.1"
|
||||
request.args[b"access_token"] = [self.test_token]
|
||||
request.args[b"user_id"] = [masquerading_user_id]
|
||||
request.args[b"org.matrix.msc3202.device_id"] = [masquerading_device_id]
|
||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
||||
|
||||
failure = self.get_failure(self.auth.get_user_by_req(request), AuthError)
|
||||
self.assertEquals(failure.value.code, 400)
|
||||
self.assertEquals(failure.value.errcode, Codes.EXCLUSIVE)
|
||||
|
||||
def test_get_user_from_macaroon(self):
|
||||
self.store.get_user_by_access_token = simple_async_mock(
|
||||
TokenLookupResult(user_id="@baldrick:matrix.org", device_id="device")
|
||||
|
Loading…
Reference in New Issue
Block a user