Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

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

Merged
merged 6 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10833.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extend the ModuleApi to let plug-ins check whether an ID is local and to access IP + User Agent data.
69 changes: 68 additions & 1 deletion synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
List,
Optional,
Tuple,
Union,
)

import attr
import jinja2

from twisted.internet import defer
Expand All @@ -46,7 +48,14 @@
from synapse.storage.database import DatabasePool, LoggingTransaction
from synapse.storage.databases.main.roommember import ProfileInfo
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.caches.descriptors import cached

Expand Down Expand Up @@ -79,6 +88,18 @@
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:
"""A proxy object that gets passed to various plugin modules so they
can register new users etc if necessary.
Expand Down Expand Up @@ -700,6 +721,52 @@ def read_templates(
(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 comes from this homeserver.
Yoric marked this conversation as resolved.
Show resolved Hide resolved

Added in Synapse v1.44.0.
Yoric marked this conversation as resolved.
Show resolved Hide resolved
"""
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: Optional[float] = 0
) -> List[UserIpAndAgent]:
"""
Return the list of user IPs and agents for a user.

Only useful for local users. If since_ts is not specified,
return the list since the epoch.
Yoric marked this conversation as resolved.
Show resolved Hide resolved

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:
Yoric marked this conversation as resolved.
Show resolved Hide resolved
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:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down
27 changes: 20 additions & 7 deletions synapse/storage/databases/main/client_ips.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,11 @@ async def get_last_client_ip_by_device(
return ret

async def get_user_ip_and_agents(
self, user: UserID
self, user: UserID, since_ts: Optional[float] = 0
babolivier marked this conversation as resolved.
Show resolved Hide resolved
) -> List[Dict[str, Union[str, int]]]:
"""
Fetch IP/User Agent connection since a given timestamp.
Yoric marked this conversation as resolved.
Show resolved Hide resolved
"""
user_id = user.to_string()
results = {}

Expand All @@ -568,13 +571,23 @@ async def get_user_ip_and_agents(
) = key
if uid == user_id:
user_agent, _, last_seen = self._batch_row_update[key]
results[(access_token, ip)] = (user_agent, last_seen)
if last_seen >= since_ts:
results[(access_token, ip)] = (user_agent, last_seen)

rows = await self.db_pool.simple_select_list(
table="user_ips",
keyvalues={"user_id": user_id},
retcols=["access_token", "ip", "user_agent", "last_seen"],
desc="get_user_ip_and_agents",
def get_recent(txn):
txn.execute(
"""
SELECT access_token, ip, user_agent, last_seen FROM user_ips
WHERE last_seen >= ? AND user_id = ?
ORDER BY last_seen
DESC
""",
(since_ts, user_id),
)
return txn.fetchall()
babolivier marked this conversation as resolved.
Show resolved Hide resolved

rows = await self.db_pool.runInteraction(
desc="get_user_ip_and_agents", func=get_recent
)

results.update(
Expand Down
39 changes: 39 additions & 0 deletions tests/module_api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def prepare(self, reactor, clock, homeserver):
self.module_api = homeserver.get_module_api()
self.event_creation_handler = homeserver.get_event_creation_handler()
self.sync_handler = homeserver.get_sync_handler()
self.auth_handler = homeserver.get_auth_handler()

def make_homeserver(self, reactor, clock):
return self.setup_test_homeserver(
Expand Down Expand Up @@ -89,6 +90,44 @@ def test_get_userinfo_by_id__no_user_found(self):
found_user = self.get_success(self.module_api.get_userinfo_by_id("@alice:test"))
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")
info = self.get_success(self.module_api.get_user_ip_and_agents(user_id))
self.assertEqual(info, [])

self.get_success(
self.store.insert_client_ip(
user_id, "access_token", "ip_1", "user_agent_1", None
)
)
self.get_success(
self.store.insert_client_ip(
user_id, "access_token", "ip_2", "user_agent_2", None
)
)
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")
elif i.ip == "ip_2":
ip_2_seen = True
self.assertEqual(i.user_agent, "user_agent_2")
self.assertTrue(ip_1_seen)
self.assertTrue(ip_2_seen)

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):
"""Tests that a module can send events into a room"""
# Mock out create_and_send_nonmember_event to check whether events are being sent
Expand Down