-
Notifications
You must be signed in to change notification settings - Fork 368
Policy server part 1: Actually call the policy server #18387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1cca9f2
a34c7af
3b4e7f9
753ce5c
d8141d8
0cd6562
f71c846
bc09886
fd78613
5984463
a27a539
08341e8
dfaf62a
5af6638
1a30ff4
aad740a
7f2d834
70987fa
f35584e
959feb9
7118d4a
eedefc7
75826a6
402fc80
ef119d6
71bec51
b676fb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add support for calling Policy Servers ([MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284)) to mark events as spam. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,6 +75,7 @@ | |
from synapse.http.types import QueryParams | ||
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace | ||
from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id | ||
from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM | ||
from synapse.util.async_helpers import concurrently_execute | ||
from synapse.util.caches.expiringcache import ExpiringCache | ||
from synapse.util.retryutils import NotRetryingDestination | ||
|
@@ -421,6 +422,62 @@ async def _record_failure_callback( | |
|
||
return None | ||
|
||
@trace | ||
@tag_args | ||
async def get_pdu_policy_recommendation( | ||
self, destination: str, pdu: EventBase, timeout: Optional[int] = None | ||
) -> str: | ||
"""Requests that the destination server (typically a policy server) | ||
check the event and return its recommendation on how to handle the | ||
event. | ||
|
||
If the policy server could not be contacted or the policy server | ||
returned an unknown recommendation, this returns an OK recommendation. | ||
This type fixing behaviour is done because the typical caller will be | ||
in a critical call path and would generally interpret a `None` or similar | ||
response as "weird value; don't care; move on without taking action". We | ||
just frontload that logic here. | ||
|
||
|
||
Args: | ||
destination: The remote homeserver to ask (a policy server) | ||
pdu: The event to check | ||
timeout: How long to try (in ms) the destination for before | ||
giving up. None indicates no timeout. | ||
|
||
Returns: | ||
The policy recommendation, or RECOMMENDATION_OK if the policy server was | ||
uncontactable or returned an unknown recommendation. | ||
""" | ||
|
||
logger.debug( | ||
"get_pdu_policy_recommendation for event_id=%s from %s", | ||
pdu.event_id, | ||
destination, | ||
) | ||
|
||
try: | ||
res = await self.transport_layer.get_policy_recommendation_for_pdu( | ||
destination, pdu, timeout=timeout | ||
) | ||
recommendation = res.get("recommendation") | ||
if not isinstance(recommendation, str): | ||
raise InvalidResponseError("recommendation is not a string") | ||
if recommendation not in (RECOMMENDATION_OK, RECOMMENDATION_SPAM): | ||
logger.warning( | ||
"get_pdu_policy_recommendation: unknown recommendation: %s", | ||
recommendation, | ||
) | ||
return RECOMMENDATION_OK | ||
turt2live marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return recommendation | ||
except Exception as e: | ||
logger.warning( | ||
"get_pdu_policy_recommendation: server %s responded with error, assuming OK recommendation: %s", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I'd rather use f-strings personally, they are waaaay more ergonomic. E.g logger.warning(f"get_pdu_policy_recommendation: server {destination} responded with error, assuming OK recommendation: {e}") There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't use f-strings in loggers, while they are more ergonomic it means that you are passing in the formatted string, rather than allowing the loggers to do so. The main consequence is that if the logger is disabled you don't necessarily want to pay the cost of string interpolation. Otherwise, yeah f-strings are awesome. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with my limited python exposure, +1 to fstrings, but definitely don't want to make things harder for the logger |
||
destination, | ||
e, | ||
) | ||
return RECOMMENDATION_OK | ||
|
||
@trace | ||
@tag_args | ||
async def get_pdu( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# | ||
# This file is licensed under the Affero General Public License (AGPL) version 3. | ||
# | ||
# Copyright 2016-2021 The Matrix.org Foundation C.I.C. | ||
# Copyright (C) 2023 New Vector, Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU Affero General Public License as | ||
# published by the Free Software Foundation, either version 3 of the | ||
# License, or (at your option) any later version. | ||
# | ||
# See the GNU Affero General Public License for more details: | ||
# <https://www.gnu.org/licenses/agpl-3.0.html>. | ||
# | ||
# | ||
|
||
import logging | ||
from typing import TYPE_CHECKING | ||
|
||
from synapse.events import EventBase | ||
from synapse.types.handlers.policy_server import RECOMMENDATION_OK | ||
from synapse.util.stringutils import parse_and_validate_server_name | ||
|
||
if TYPE_CHECKING: | ||
from synapse.server import HomeServer | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class RoomPolicyHandler: | ||
def __init__(self, hs: "HomeServer"): | ||
self._hs = hs | ||
self._store = hs.get_datastores().main | ||
self._storage_controllers = hs.get_storage_controllers() | ||
self._event_auth_handler = hs.get_event_auth_handler() | ||
self._federation_client = hs.get_federation_client() | ||
|
||
async def is_event_allowed(self, event: EventBase) -> bool: | ||
"""Check if the given event is allowed in the room by the policy server. | ||
|
||
Note: This will *always* return True if the room's policy server is Synapse | ||
itself. This is because Synapse can't be a policy server (currently). | ||
|
||
If no policy server is configured in the room, this returns True. Similarly, if | ||
the policy server is invalid in any way (not joined, not a server, etc), this | ||
returns True. | ||
|
||
If a valid and contactable policy server is configured in the room, this returns | ||
True if that server suggests the event is not spammy, and False otherwise. | ||
|
||
Args: | ||
event: The event to check. This should be a fully-formed PDU. | ||
|
||
Returns: | ||
bool: True if the event is allowed in the room, False otherwise. | ||
""" | ||
policy_event = await self._storage_controllers.state.get_current_state_event( | ||
event.room_id, "org.matrix.msc4284.policy", "" | ||
) | ||
if not policy_event: | ||
return True # no policy server == default allow | ||
|
||
policy_server = policy_event.content.get("via", "") | ||
if policy_server is None or not isinstance(policy_server, str): | ||
return True # no policy server == default allow | ||
|
||
if policy_server == self._hs.hostname: | ||
return True # Synapse itself can't be a policy server (currently) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And it can't be run on the same domain with a .well-known lookup or something? I'd rather we didn't hard code this given if/when Synapse can be a PS we need to remember to remove this specific conditional instead of just intuitively pointing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not entirely sure that Synapse will be happy with trying to contact itself over federation because that's a pretty weird thing to do in Matrix. Any Synapse support for being a policy server (either natively or via modules) would involve a performant hook anyway, and would be inserted here to short-circuit the federation call. Removing the check would also obviously slow down message sending in rooms we know are misconfigured (not pointing to a real policy server). With the current highly experimental architecture we also do not expect that pre-existing servers will be policy servers, and operators will need to pick a dedicated domain name without human users on that server. This will probably change down the line. I'll add some text to the docstring to reduce the chances of forgetting in the meantime. Hopefully when someone writes a test for "can Synapse be a policy server", they'll worst case find the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also ended up adding a test for this, so the failure should be even more clear if we do add such functionality |
||
|
||
try: | ||
parse_and_validate_server_name(policy_server) | ||
except ValueError: | ||
return True # invalid policy server == default allow | ||
|
||
is_in_room = await self._event_auth_handler.is_host_in_room( | ||
event.room_id, policy_server | ||
) | ||
if not is_in_room: | ||
return True # policy server not in room == default allow | ||
|
||
# At this point, the server appears valid and is in the room, so ask it to check | ||
# the event. | ||
recommendation = await self._federation_client.get_pdu_policy_recommendation( | ||
policy_server, event | ||
) | ||
if recommendation != RECOMMENDATION_OK: | ||
return False | ||
turt2live marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return True # default allow |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# | ||
# This file is licensed under the Affero General Public License (AGPL) version 3. | ||
# | ||
# Copyright (C) 2025 New Vector, Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU Affero General Public License as | ||
# published by the Free Software Foundation, either version 3 of the | ||
# License, or (at your option) any later version. | ||
# | ||
# See the GNU Affero General Public License for more details: | ||
# <https://www.gnu.org/licenses/agpl-3.0.html>. | ||
# | ||
|
||
RECOMMENDATION_OK = "ok" | ||
RECOMMENDATION_SPAM = "spam" |
Uh oh!
There was an error while loading. Please reload this page.