Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a7272e5
Remove `/check` code (legacy implementation)
turt2live Feb 26, 2026
842454a
Raise transport errors with policy server to calling code
turt2live Feb 26, 2026
016898d
Refactor room policy handler to match MSC4284
turt2live Feb 26, 2026
115658e
Call stable `/sign` and fall back to unstable
turt2live Feb 26, 2026
ac4051a
Update tests to match stable behaviour
turt2live Feb 26, 2026
ec80ab0
Attempt to fix linting
turt2live Feb 26, 2026
b522575
changelog
turt2live Feb 26, 2026
b396cf5
Fix tuples usage
turt2live Feb 26, 2026
41baca6
Fix function naming
turt2live Mar 3, 2026
77e8a66
Merge branch 'develop' into travis/stable-4284-policy-servers
turt2live Mar 4, 2026
16ec6f0
Fix check for state keys
turt2live Mar 4, 2026
7a9a866
Maybe we should use constants instead
turt2live Mar 4, 2026
18bbe7a
debugging in production
turt2live Mar 4, 2026
af8b2e3
Fix policy server being asked to sign events it shouldn't be
turt2live Mar 4, 2026
2e52493
Apply signatures more safely
turt2live Mar 4, 2026
9a60073
Attempt to fix linting
turt2live Mar 4, 2026
8d498ae
Add more changelogs
turt2live Mar 4, 2026
bbe5e95
Wrap policy server errors for client delivery
turt2live Mar 4, 2026
29ad925
Attempt to fix linting
turt2live Mar 4, 2026
df48e68
bump for ci
turt2live Mar 4, 2026
9008cf0
Merge branch 'develop' into travis/stable-4284-policy-servers
turt2live Mar 10, 2026
065e516
Fix tests to handle errors instead of quiet success
turt2live Mar 10, 2026
cab972e
Attempt to fix linting
turt2live Mar 10, 2026
04b12e8
bump for ci
turt2live Mar 10, 2026
d7ef91f
Copilot AI review suggestions
turt2live Mar 18, 2026
6689494
Apply suggestions from code review
turt2live Mar 19, 2026
8de3736
Simplify(?) types around `get_policy_server`
turt2live Mar 20, 2026
5b3d6ef
Clarify comments
turt2live Mar 20, 2026
470636f
appease type checking
turt2live Mar 20, 2026
0967ebd
Use a carrier type for the policy server info
turt2live Mar 20, 2026
3e40ab2
Clarify comments again
turt2live Mar 20, 2026
742d4b0
Attempt to fix linting
turt2live Mar 20, 2026
368e383
bump ci
turt2live Mar 20, 2026
81ecc91
clarify again
turt2live Mar 20, 2026
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/19503.bugfix.1
Comment thread
MadLittleMods marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers implementation to skip signing `org.matrix.msc4284.policy` and `m.room.policy` state events.
1 change: 1 addition & 0 deletions changelog.d/19503.bugfix.2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Correctly apply [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Server signatures to events when the sender and policy server have the same server name.
1 change: 1 addition & 0 deletions changelog.d/19503.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add stable support for [MSC4284](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) Policy Servers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Membership event handling (from the issue) is expected to be a different PR due to the size of changes involved (tracked by #19587).

Seems a bit weird to make this stable if #19587 isn't solved yet.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's been broken since inception in April 2025, so I'm not that concerned about it being a bug for a few more weeks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To close the loop of comments: #19593 is the start of this work, but requires this PR to land before it can make much more progress.

2 changes: 2 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ class EventTypes:

PollStart: Final = "m.poll.start"

RoomPolicy: Final = "m.room.policy"
Comment thread
MadLittleMods marked this conversation as resolved.


class ToDeviceEventTypes:
RoomKeyRequest: Final = "m.room_key_request"
Expand Down
75 changes: 5 additions & 70 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace
from synapse.metrics import SERVER_NAME_LABEL
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.duration import Duration
Expand Down Expand Up @@ -438,72 +437,16 @@ async def _record_failure_callback(

return None

@trace
@tag_args
async def get_pdu_policy_recommendation(
self, destination: str, pdu: EventBase, timeout: int | None = 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
return recommendation
except Exception as e:
logger.warning(
"get_pdu_policy_recommendation: server %s responded with error, assuming OK recommendation: %s",
destination,
e,
)
return RECOMMENDATION_OK

@trace
@tag_args
async def ask_policy_server_to_sign_event(
self, destination: str, pdu: EventBase, timeout: int | None = None
) -> JsonDict | None:
) -> JsonDict:
"""Requests that the destination server (typically a policy server)
sign the event as not spam.
Comment thread
turt2live marked this conversation as resolved.

If the policy server could not be contacted or the policy server
returned an error, this returns no signature.
returned an error, that error is raised.

Args:
destination: The remote homeserver to ask (a policy server)
Expand All @@ -519,17 +462,9 @@ async def ask_policy_server_to_sign_event(
pdu.event_id,
destination,
)
try:
return await self.transport_layer.ask_policy_server_to_sign_event(
destination, pdu, timeout=timeout
)
except Exception as e:
logger.warning(
"ask_policy_server_to_sign_event: server %s responded with error: %s",
destination,
e,
)
return None
return await self.transport_layer.ask_policy_server_to_sign_event(
destination, pdu, timeout=timeout
)

@trace
@tag_args
Expand Down
57 changes: 23 additions & 34 deletions synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
)
from synapse.events import EventBase, make_event_from_dict
from synapse.federation.units import Transaction
from synapse.http.client import is_unknown_endpoint
from synapse.http.matrixfederationclient import ByteParser, LegacyJsonSendParser
from synapse.http.types import QueryParams
from synapse.types import JsonDict, UserID
Expand Down Expand Up @@ -141,33 +142,6 @@ async def get_event(
destination, path=path, timeout=timeout, try_trailing_slash_on_400=True
)

async def get_policy_recommendation_for_pdu(
self, destination: str, event: EventBase, timeout: int | None = None
) -> JsonDict:
"""Requests the policy recommendation for the given pdu from the given policy server.

Args:
destination: The host name of the remote homeserver checking the event.
event: The event to check.
timeout: How long to try (in ms) the destination for before giving up.
None indicates no timeout.

Returns:
The full recommendation object from the remote server.
"""
logger.debug(
"get_policy_recommendation_for_pdu dest=%s, event_id=%s",
destination,
event.event_id,
)
return await self.client.post_json(
destination=destination,
path=f"/_matrix/policy/unstable/org.matrix.msc4284/event/{event.event_id}/check",
data=event.get_pdu_json(),
ignore_backoff=True,
timeout=timeout,
)

async def ask_policy_server_to_sign_event(
self, destination: str, event: EventBase, timeout: int | None = None
) -> JsonDict:
Expand All @@ -186,13 +160,28 @@ async def ask_policy_server_to_sign_event(
The signature from the policy server, structured in the same was as the 'signatures'
JSON in the event e.g { "$policy_server_via_domain" : { "ed25519:policy_server": "signature_base64" }}
"""
return await self.client.post_json(
destination=destination,
path="/_matrix/policy/unstable/org.matrix.msc4284/sign",
data=event.get_pdu_json(),
ignore_backoff=True,
timeout=timeout,
)
# Try stable first, then fall back to unstable if unsupported. All other errors
# are just errors.
try:
return await self.client.post_json(
destination=destination,
path="/_matrix/policy/v1/sign",
data=event.get_pdu_json(),
ignore_backoff=True,
timeout=timeout,
)
except HttpResponseException as ex:
if is_unknown_endpoint(ex):
# TODO: Remove unstable MSC4284 support
# https://github.com/element-hq/synapse/issues/19502
return await self.client.post_json(
destination=destination,
path="/_matrix/policy/unstable/org.matrix.msc4284/sign",
data=event.get_pdu_json(),
ignore_backoff=True,
timeout=timeout,
)
raise

async def backfill(
self, destination: str, room_id: str, event_tuples: Collection[str], limit: int
Expand Down
Loading
Loading