Extend ModuleApi with the methods we'll need to reject spam based on …IP - resolves #10832 (#10833)

Extend ModuleApi with the methods we'll need to reject spam based on IP - resolves #10832

Signed-off-by: David Teller <davidt@element.io>
pull/10889/head
David Teller 2021-09-22 15:09:43 +02:00 committed by GitHub
parent 4ecf51812e
commit 80828eda06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 174 additions and 8 deletions

1
changelog.d/10833.misc Normal file
View File

@ -0,0 +1 @@
Extend the ModuleApi to let plug-ins check whether an ID is local and to access IP + User Agent data.

View File

@ -24,8 +24,10 @@ from typing import (
List, List,
Optional, Optional,
Tuple, Tuple,
Union,
) )
import attr
import jinja2 import jinja2
from twisted.internet import defer from twisted.internet import defer
@ -46,7 +48,14 @@ from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage.database import DatabasePool, LoggingTransaction from synapse.storage.database import DatabasePool, LoggingTransaction
from synapse.storage.databases.main.roommember import ProfileInfo from synapse.storage.databases.main.roommember import ProfileInfo
from synapse.storage.state import StateFilter from synapse.storage.state import StateFilter
from synapse.types import JsonDict, Requester, UserID, UserInfo, create_requester from synapse.types import (
DomainSpecificString,
JsonDict,
Requester,
UserID,
UserInfo,
create_requester,
)
from synapse.util import Clock from synapse.util import Clock
from synapse.util.caches.descriptors import cached from synapse.util.caches.descriptors import cached
@ -79,6 +88,18 @@ __all__ = [
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@attr.s(auto_attribs=True)
class UserIpAndAgent:
"""
An IP address and user agent used by a user to connect to this homeserver.
"""
ip: str
user_agent: str
# The time at which this user agent/ip was last seen.
last_seen: int
class ModuleApi: class ModuleApi:
"""A proxy object that gets passed to various plugin modules so they """A proxy object that gets passed to various plugin modules so they
can register new users etc if necessary. can register new users etc if necessary.
@ -700,6 +721,65 @@ class ModuleApi:
(td for td in (self.custom_template_dir, custom_template_directory) if td), (td for td in (self.custom_template_dir, custom_template_directory) if td),
) )
def is_mine(self, id: Union[str, DomainSpecificString]) -> bool:
"""
Checks whether an ID (user id, room, ...) comes from this homeserver.
Args:
id: any Matrix id (e.g. user id, room id, ...), either as a raw id,
e.g. string "@user:example.com" or as a parsed UserID, RoomID, ...
Returns:
True if id comes from this homeserver, False otherwise.
Added in Synapse v1.44.0.
"""
if isinstance(id, DomainSpecificString):
return self._hs.is_mine(id)
else:
return self._hs.is_mine_id(id)
async def get_user_ip_and_agents(
self, user_id: str, since_ts: int = 0
) -> List[UserIpAndAgent]:
"""
Return the list of user IPs and agents for a user.
Args:
user_id: the id of a user, local or remote
since_ts: a timestamp in seconds since the epoch,
or the epoch itself if not specified.
Returns:
The list of all UserIpAndAgent that the user has
used to connect to this homeserver since `since_ts`.
If the user is remote, this list is empty.
Added in Synapse v1.44.0.
"""
# Don't hit the db if this is not a local user.
is_mine = False
try:
# Let's be defensive against ill-formed strings.
if self.is_mine(user_id):
is_mine = True
except Exception:
pass
if is_mine:
raw_data = await self._store.get_user_ip_and_agents(
UserID.from_string(user_id), since_ts
)
# Sanitize some of the data. We don't want to return tokens.
return [
UserIpAndAgent(
ip=str(data["ip"]),
user_agent=str(data["user_agent"]),
last_seen=int(data["last_seen"]),
)
for data in raw_data
]
else:
return []
class PublicRoomListManager: class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room """Contains methods for adding to, removing from and querying whether a room

View File

@ -555,8 +555,11 @@ class ClientIpStore(ClientIpWorkerStore):
return ret return ret
async def get_user_ip_and_agents( async def get_user_ip_and_agents(
self, user: UserID self, user: UserID, since_ts: int = 0
) -> List[Dict[str, Union[str, int]]]: ) -> List[Dict[str, Union[str, int]]]:
"""
Fetch IP/User Agent connection since a given timestamp.
"""
user_id = user.to_string() user_id = user.to_string()
results = {} results = {}
@ -568,13 +571,23 @@ class ClientIpStore(ClientIpWorkerStore):
) = key ) = key
if uid == user_id: if uid == user_id:
user_agent, _, last_seen = self._batch_row_update[key] user_agent, _, last_seen = self._batch_row_update[key]
if last_seen >= since_ts:
results[(access_token, ip)] = (user_agent, last_seen) results[(access_token, ip)] = (user_agent, last_seen)
rows = await self.db_pool.simple_select_list( def get_recent(txn):
table="user_ips", txn.execute(
keyvalues={"user_id": user_id}, """
retcols=["access_token", "ip", "user_agent", "last_seen"], SELECT access_token, ip, user_agent, last_seen FROM user_ips
desc="get_user_ip_and_agents", WHERE last_seen >= ? AND user_id = ?
ORDER BY last_seen
DESC
""",
(since_ts, user_id),
)
return txn.fetchall()
rows = await self.db_pool.runInteraction(
desc="get_user_ip_and_agents", func=get_recent
) )
results.update( results.update(

View File

@ -43,6 +43,7 @@ class ModuleApiTestCase(HomeserverTestCase):
self.module_api = homeserver.get_module_api() self.module_api = homeserver.get_module_api()
self.event_creation_handler = homeserver.get_event_creation_handler() self.event_creation_handler = homeserver.get_event_creation_handler()
self.sync_handler = homeserver.get_sync_handler() self.sync_handler = homeserver.get_sync_handler()
self.auth_handler = homeserver.get_auth_handler()
def make_homeserver(self, reactor, clock): def make_homeserver(self, reactor, clock):
return self.setup_test_homeserver( return self.setup_test_homeserver(
@ -89,6 +90,77 @@ class ModuleApiTestCase(HomeserverTestCase):
found_user = self.get_success(self.module_api.get_userinfo_by_id("@alice:test")) found_user = self.get_success(self.module_api.get_userinfo_by_id("@alice:test"))
self.assertIsNone(found_user) self.assertIsNone(found_user)
def test_get_user_ip_and_agents(self):
user_id = self.register_user("test_get_user_ip_and_agents_user", "1234")
# Initially, we should have no ip/agent for our user.
info = self.get_success(self.module_api.get_user_ip_and_agents(user_id))
self.assertEqual(info, [])
# Insert a first ip, agent. We should be able to retrieve it.
self.get_success(
self.store.insert_client_ip(
user_id, "access_token", "ip_1", "user_agent_1", "device_1", None
)
)
info = self.get_success(self.module_api.get_user_ip_and_agents(user_id))
self.assertEqual(len(info), 1)
last_seen_1 = info[0].last_seen
# Insert a second ip, agent at a later date. We should be able to retrieve it.
last_seen_2 = last_seen_1 + 10000
print("%s => %s" % (last_seen_1, last_seen_2))
self.get_success(
self.store.insert_client_ip(
user_id, "access_token", "ip_2", "user_agent_2", "device_2", last_seen_2
)
)
info = self.get_success(self.module_api.get_user_ip_and_agents(user_id))
self.assertEqual(len(info), 2)
ip_1_seen = False
ip_2_seen = False
for i in info:
if i.ip == "ip_1":
ip_1_seen = True
self.assertEqual(i.user_agent, "user_agent_1")
self.assertEqual(i.last_seen, last_seen_1)
elif i.ip == "ip_2":
ip_2_seen = True
self.assertEqual(i.user_agent, "user_agent_2")
self.assertEqual(i.last_seen, last_seen_2)
self.assertTrue(ip_1_seen)
self.assertTrue(ip_2_seen)
# If we fetch from a midpoint between last_seen_1 and last_seen_2,
# we should only find the second ip, agent.
info = self.get_success(
self.module_api.get_user_ip_and_agents(
user_id, (last_seen_1 + last_seen_2) / 2
)
)
self.assertEqual(len(info), 1)
self.assertEqual(info[0].ip, "ip_2")
self.assertEqual(info[0].user_agent, "user_agent_2")
self.assertEqual(info[0].last_seen, last_seen_2)
# If we fetch from a point later than last_seen_2, we shouldn't
# find anything.
info = self.get_success(
self.module_api.get_user_ip_and_agents(user_id, last_seen_2 + 10000)
)
self.assertEqual(info, [])
def test_get_user_ip_and_agents__no_user_found(self):
info = self.get_success(
self.module_api.get_user_ip_and_agents(
"@test_get_user_ip_and_agents_user_nonexistent:example.com"
)
)
self.assertEqual(info, [])
def test_sending_events_into_room(self): def test_sending_events_into_room(self):
"""Tests that a module can send events into a room""" """Tests that a module can send events into a room"""
# Mock out create_and_send_nonmember_event to check whether events are being sent # Mock out create_and_send_nonmember_event to check whether events are being sent