Skip to content

Commit c491633

Browse files
committed
MSC4380 implementation
An implementation of MSC4380, leaning heavily on what's already there for MSC4155. It has its own `experimental_features` flag. If both MSC4155 and MSC4380 are enabled, and a user has both configurations set, then we prioritise the MSC4380 one.
1 parent 9ca692e commit c491633

File tree

7 files changed

+183
-2
lines changed

7 files changed

+183
-2
lines changed

synapse/api/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ class AccountDataTypes:
307307
MSC4155_INVITE_PERMISSION_CONFIG: Final = (
308308
"org.matrix.msc4155.invite_permission_config"
309309
)
310+
# MSC4380: Invite blocking
311+
MSC4380_INVITE_PERMISSION_CONFIG: Final = (
312+
"org.matrix.msc4380.invite_permission_config"
313+
)
310314
# Synapse-specific behaviour. See "Client-Server API Extensions" documentation
311315
# in Admin API for more information.
312316
SYNAPSE_ADMIN_CLIENT_CONFIG: Final = "io.element.synapse.admin_client_config"

synapse/api/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class Codes(str, Enum):
137137
PROFILE_TOO_LARGE = "M_PROFILE_TOO_LARGE"
138138
KEY_TOO_LARGE = "M_KEY_TOO_LARGE"
139139

140-
# Part of MSC4155
140+
# Part of MSC4155/MSC4380
141141
INVITE_BLOCKED = "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED"
142142

143143
# Part of MSC4190

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,3 +593,6 @@ def read_config(
593593
# MSC4306: Thread Subscriptions
594594
# (and MSC4308: Thread Subscriptions extension to Sliding Sync)
595595
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)
596+
597+
# MSC4380: Invite blocking
598+
self.msc4380_enabled: bool = experimental.get("msc4380_enabled", False)

synapse/rest/client/versions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
182182
"org.matrix.msc4306": self.config.experimental.msc4306_enabled,
183183
# MSC4169: Backwards-compatible redaction sending using `/send`
184184
"com.beeper.msc4169": self.config.experimental.msc4169_enabled,
185+
# MSC4380: Invite blocking
186+
"org.matrix.msc4380": self.config.experimental.msc4380_enabled,
185187
},
186188
},
187189
)

synapse/storage/databases/main/account_data.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from synapse.storage.invite_rule import (
4444
InviteRulesConfig,
4545
MSC4155InviteRulesConfig,
46+
MSC4380InviteRulesConfig,
4647
AllowAllInviteRulesConfig,
4748
)
4849
from synapse.storage.util.id_generators import MultiWriterIdGenerator
@@ -108,6 +109,7 @@ def __init__(
108109
)
109110

110111
self._msc4155_enabled = hs.config.experimental.msc4155_enabled
112+
self._msc4380_enabled = hs.config.experimental.msc4380_enabled
111113

112114
def get_max_account_data_stream_id(self) -> int:
113115
"""Get the current max stream ID for account data stream
@@ -571,6 +573,15 @@ async def get_invite_config_for_user(self, user_id: str) -> InviteRulesConfig:
571573
Args:
572574
user_id: The user whose invite configuration should be returned.
573575
"""
576+
if self._msc4380_enabled:
577+
data = await self.get_global_account_data_by_type_for_user(
578+
user_id, AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG
579+
)
580+
# If the user has an MSC4380-style config setting, prioritise that
581+
# above an MSC4155 one
582+
if data is not None:
583+
return MSC4380InviteRulesConfig.from_account_data(data)
584+
574585
if self._msc4155_enabled:
575586
data = await self.get_global_account_data_by_type_for_user(
576587
user_id, AccountDataTypes.MSC4155_INVITE_PERMISSION_CONFIG

synapse/storage/invite_rule.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,20 @@ def get_invite_rule(self, user_id: str) -> InviteRule:
130130
return rule
131131

132132
return InviteRule.ALLOW
133+
134+
135+
@attr.s(slots=True, auto_attribs=True)
136+
class MSC4380InviteRulesConfig(InviteRulesConfig):
137+
block_all: bool
138+
"""If true, all invites are blocked."""
139+
140+
@classmethod
141+
def from_account_data(cls, data: JsonMapping) -> "MSC4380InviteRulesConfig":
142+
block_all = data.get("block_all")
143+
if not isinstance(block_all, bool):
144+
block_all = False
145+
146+
return cls(block_all=block_all)
147+
148+
def get_invite_rule(self, inviter_user_id: str) -> InviteRule:
149+
return InviteRule.BLOCK if self.block_all else InviteRule.ALLOW

tests/handlers/test_room_member.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,9 @@ def test_deduplicate_joins(self) -> None:
458458
self.assertEqual(initial_count, new_count)
459459

460460

461-
class TestInviteFiltering(FederatingHomeserverTestCase):
461+
class TestMSC4155InviteFiltering(FederatingHomeserverTestCase):
462+
"""Tests for MSC4155-style invite filtering."""
463+
462464
servlets = [
463465
synapse.rest.admin.register_servlets,
464466
synapse.rest.client.login.register_servlets,
@@ -618,3 +620,145 @@ def test_msc4155_block_invite_remote_server(self) -> None:
618620
).value
619621
self.assertEqual(f.code, 403)
620622
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
623+
624+
625+
class TestMSC4380InviteFiltering(FederatingHomeserverTestCase):
626+
"""Tests for MSC4380-style invite filtering."""
627+
628+
servlets = [
629+
synapse.rest.admin.register_servlets,
630+
synapse.rest.client.login.register_servlets,
631+
synapse.rest.client.room.register_servlets,
632+
]
633+
634+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
635+
self.handler = hs.get_room_member_handler()
636+
self.fed_handler = hs.get_federation_handler()
637+
self.store = hs.get_datastores().main
638+
639+
# Create two users.
640+
self.alice = self.register_user("alice", "pass")
641+
self.alice_token = self.login("alice", "pass")
642+
self.bob = self.register_user("bob", "pass")
643+
self.bob_token = self.login("bob", "pass")
644+
645+
@override_config({"experimental_features": {"msc4380_enabled": True}})
646+
def test_misc4380_block_invite_local(self) -> None:
647+
"""Test that MSC4380 will block a user from being invited to a room"""
648+
room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)
649+
650+
self.get_success(
651+
self.store.add_account_data_for_user(
652+
self.bob,
653+
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
654+
{
655+
"block_all": True,
656+
},
657+
)
658+
)
659+
660+
f = self.get_failure(
661+
self.handler.update_membership(
662+
requester=create_requester(self.alice),
663+
target=UserID.from_string(self.bob),
664+
room_id=room_id,
665+
action=Membership.INVITE,
666+
),
667+
SynapseError,
668+
).value
669+
self.assertEqual(f.code, 403)
670+
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
671+
672+
@override_config({"experimental_features": {"msc4380_enabled": True}})
673+
def test_misc4380_non_bool_setting(self) -> None:
674+
"""Test that `block_all` being set to something non-booly is the same as False."""
675+
room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)
676+
677+
self.get_success(
678+
self.store.add_account_data_for_user(
679+
self.bob,
680+
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
681+
{
682+
"block_all": "True",
683+
},
684+
)
685+
)
686+
687+
self.get_success(
688+
self.handler.update_membership(
689+
requester=create_requester(self.alice),
690+
target=UserID.from_string(self.bob),
691+
room_id=room_id,
692+
action=Membership.INVITE,
693+
)
694+
)
695+
696+
@override_config({"experimental_features": {"msc4380_enabled": False}})
697+
def test_msc4380_disabled_allow_invite_local(self) -> None:
698+
"""Test that MSC4380 will block a user from being invited to a room"""
699+
room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)
700+
701+
self.get_success(
702+
self.store.add_account_data_for_user(
703+
self.bob,
704+
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
705+
{
706+
"block_all": True,
707+
},
708+
)
709+
)
710+
711+
self.get_success(
712+
self.handler.update_membership(
713+
requester=create_requester(self.alice),
714+
target=UserID.from_string(self.bob),
715+
room_id=room_id,
716+
action=Membership.INVITE,
717+
),
718+
)
719+
720+
@override_config({"experimental_features": {"msc4380_enabled": True}})
721+
def test_msc4380_block_invite_remote(self) -> None:
722+
"""Test that MSC4380 will block a user from being invited to a room by a remote user."""
723+
# A remote user who sends the invite
724+
remote_server = "otherserver"
725+
remote_user = "@otheruser:" + remote_server
726+
727+
self.get_success(
728+
self.store.add_account_data_for_user(
729+
self.bob,
730+
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
731+
{"block_all": True},
732+
)
733+
)
734+
735+
room_id = self.helper.create_room_as(
736+
room_creator=self.alice, tok=self.alice_token
737+
)
738+
room_version = self.get_success(self.store.get_room_version(room_id))
739+
740+
invite_event = event_from_pdu_json(
741+
{
742+
"type": EventTypes.Member,
743+
"content": {"membership": "invite"},
744+
"room_id": room_id,
745+
"sender": remote_user,
746+
"state_key": self.bob,
747+
"depth": 32,
748+
"prev_events": [],
749+
"auth_events": [],
750+
"origin_server_ts": self.clock.time_msec(),
751+
},
752+
room_version,
753+
)
754+
755+
f = self.get_failure(
756+
self.fed_handler.on_invite_request(
757+
remote_server,
758+
invite_event,
759+
invite_event.room_version,
760+
),
761+
SynapseError,
762+
).value
763+
self.assertEqual(f.code, 403)
764+
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")

0 commit comments

Comments
 (0)