mirror of
https://git.anonymousland.org/anonymousland/synapse-product.git
synced 2025-01-06 18:27:55 -05: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)
|
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 user_id and app_service:
|
||||||
if ip_addr and self._track_appservice_user_ips:
|
if ip_addr and self._track_appservice_user_ips:
|
||||||
await self.store.insert_client_ip(
|
await self.store.insert_client_ip(
|
||||||
@ -163,16 +167,22 @@ class Auth:
|
|||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
ip=ip_addr,
|
ip=ip_addr,
|
||||||
user_agent=user_agent,
|
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
|
request.requester = user_id
|
||||||
if user_id in self._force_tracing_for_users:
|
if user_id in self._force_tracing_for_users:
|
||||||
opentracing.force_tracing()
|
opentracing.force_tracing()
|
||||||
opentracing.set_tag("authenticated_entity", user_id)
|
opentracing.set_tag("authenticated_entity", user_id)
|
||||||
opentracing.set_tag("user_id", 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)
|
opentracing.set_tag("appservice_id", app_service.id)
|
||||||
|
|
||||||
return requester
|
return requester
|
||||||
@ -274,33 +284,81 @@ class Auth:
|
|||||||
403, "Application service has not registered this user (%s)" % user_id
|
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
|
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(
|
app_service = self.store.get_app_service_by_token(
|
||||||
self.get_access_token_from_request(request)
|
self.get_access_token_from_request(request)
|
||||||
)
|
)
|
||||||
if app_service is None:
|
if app_service is None:
|
||||||
return None, None
|
return None, None, None
|
||||||
|
|
||||||
if app_service.ip_range_whitelist:
|
if app_service.ip_range_whitelist:
|
||||||
ip_address = IPAddress(request.getClientIP())
|
ip_address = IPAddress(request.getClientIP())
|
||||||
if ip_address not in app_service.ip_range_whitelist:
|
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.
|
# This will always be set by the time Twisted calls us.
|
||||||
assert request.args is not None
|
assert request.args is not None
|
||||||
|
|
||||||
if b"user_id" not in request.args:
|
if b"user_id" in request.args:
|
||||||
return app_service.sender, app_service
|
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")
|
effective_device_id: Optional[str] = None
|
||||||
await self.validate_appservice_can_control_user_id(app_service, user_id)
|
|
||||||
|
|
||||||
if app_service.sender == user_id:
|
if (
|
||||||
return app_service.sender, app_service
|
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(
|
async def get_user_by_access_token(
|
||||||
self,
|
self,
|
||||||
|
@ -49,3 +49,8 @@ class ExperimentalConfig(Config):
|
|||||||
|
|
||||||
# MSC3030 (Jump to date API endpoint)
|
# MSC3030 (Jump to date API endpoint)
|
||||||
self.msc3030_enabled: bool = experimental.get("msc3030_enabled", False)
|
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,
|
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]]:
|
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
|
"""Retrieve all of a user's registered devices. Only returns devices
|
||||||
that are not marked as hidden.
|
that are not marked as hidden.
|
||||||
|
@ -31,6 +31,7 @@ from synapse.types import Requester
|
|||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
from tests.test_utils import simple_async_mock
|
from tests.test_utils import simple_async_mock
|
||||||
|
from tests.unittest import override_config
|
||||||
from tests.utils import mock_getRawHeaders
|
from tests.utils import mock_getRawHeaders
|
||||||
|
|
||||||
|
|
||||||
@ -210,6 +211,69 @@ class AuthTestCase(unittest.HomeserverTestCase):
|
|||||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
||||||
self.get_failure(self.auth.get_user_by_req(request), AuthError)
|
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):
|
def test_get_user_from_macaroon(self):
|
||||||
self.store.get_user_by_access_token = simple_async_mock(
|
self.store.get_user_by_access_token = simple_async_mock(
|
||||||
TokenLookupResult(user_id="@baldrick:matrix.org", device_id="device")
|
TokenLookupResult(user_id="@baldrick:matrix.org", device_id="device")
|
||||||
|
Loading…
Reference in New Issue
Block a user