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

Allow ThirdPartyEventRules modules to manipulate public room state #8292

Merged
merged 23 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
68391e4
Add a store method for checking whether a room is in the public dir
anoadragon453 Sep 10, 2020
0503eee
Allow ThirdPartyEventRules modules to block publishing a room
anoadragon453 Sep 10, 2020
9eae87d
Query ThirdPartyEventRules about public rooms during room creation
anoadragon453 Sep 10, 2020
833883e
Query ThirdPartyEventRules while trying to add to the public room dir
anoadragon453 Sep 10, 2020
8690c86
Add tests
anoadragon453 Sep 11, 2020
6eae2c1
Changelog
anoadragon453 Sep 11, 2020
431e3e3
Replace verbose type with StateMap
anoadragon453 Sep 11, 2020
529863a
Remove get_room_is_public and use get_room instead
anoadragon453 Sep 11, 2020
699fa5b
Address review comments
anoadragon453 Sep 11, 2020
f5b6246
Axe the PublicRoomsManager and switch to passing the ModuleApi
anoadragon453 Sep 11, 2020
e768fae
Move PublicRoomsManager tests to ModuleApi
anoadragon453 Sep 11, 2020
04ab21c
Create a PublicRoomListManager as part of ModuleApi
anoadragon453 Sep 14, 2020
06deecf
Backwards incompatibility warning for ThirdPartyEventRules in UPGRADE…
anoadragon453 Sep 14, 2020
6ed082d
Some minor fixes
anoadragon453 Sep 14, 2020
83a20a2
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/thi…
anoadragon453 Sep 14, 2020
0f4c1bb
Update unit tests
anoadragon453 Sep 14, 2020
3bde820
Fix call to old method name
anoadragon453 Sep 14, 2020
8d9b166
Update UPGRADE.rst, remove extra entry, convert to rst
anoadragon453 Sep 28, 2020
1ff7df9
Move PublicRoomListManager below ModuleApi
anoadragon453 Sep 28, 2020
e5b2d20
Remove hs param from the PublicRoomListManager docstring
anoadragon453 Sep 28, 2020
5730d17
Expose http_client and public_room_list_manager as properties
anoadragon453 Sep 28, 2020
d46396d
Merge branch 'develop' of github.com:matrix-org/synapse into anoa/thi…
anoadragon453 Oct 5, 2020
49e71a8
Move upgrade notes comment from v1.21.0 to v1.22.0
anoadragon453 Oct 5, 2020
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/8292.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow ThirdPartyEventRules modules to query and manipulate whether a room is in the public rooms directory.
80 changes: 75 additions & 5 deletions synapse/events/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,45 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Tuple

from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.types import Requester


class PublicRoomsManager:
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, hs):
self._store = hs.get_datastore()

async def room_is_in_public_directory(self, room_id: str) -> bool:
"""Checks whether a room is in the public rooms directory.

Args:
room_id: The ID of the room.

Returns:
Whether the room is in the public rooms directory.
"""
return await self._store.get_room_is_public(room_id)

async def add_room_to_public_directory(self, room_id: str) -> None:
"""Publishes a room to the public rooms directory.

Args:
room_id: The ID of the room.
"""
await self._store.set_room_is_public(room_id, True)

async def remove_room_from_public_directory(self, room_id: str) -> None:
"""Removes a room from the public rooms directory.

Args:
room_id: The ID of the room.
"""
await self._store.set_room_is_public(room_id, False)


class ThirdPartyEventRules:
"""Allows server admins to provide a Python module implementing an extra
set of rules to apply when processing events.
Expand All @@ -38,7 +71,9 @@ def __init__(self, hs):

if module is not None:
self.third_party_rules = module(
config=config, http_client=hs.get_simple_http_client()
config=config,
http_client=hs.get_simple_http_client(),
public_room_manager=PublicRoomsManager(hs),
Copy link
Member

Choose a reason for hiding this comment

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

this will break compat with classes which don't have a public_room_manager parameter on their constructor?

maybe in this case it's best to just document this as a breaking change. But if we're going to do so can we make it future-proof by passing in a moduleapi and getting rid of http_client (possibly by making the simplehttpclient accessible via the moduleapi).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, need to stick a note in about the breaking change.

)

async def check_event_allowed(
Expand Down Expand Up @@ -106,14 +141,49 @@ async def check_threepid_can_be_invited(
if self.third_party_rules is None:
return True

state_events = await self._get_state_events_dict_for_room(room_id)

ret = await self.third_party_rules.check_threepid_can_be_invited(
medium, address, state_events
)
return ret

async def check_room_can_be_added_to_public_rooms_directory(
self, room_id: str
) -> bool:
"""Check if a room is allowed to be published to the public rooms directory.

Args:
room_id: The ID of the room.

Returns:
Whether the room is allowed to be published to the public rooms directory.
"""
if self.third_party_rules is None:
return True

state_events = await self._get_state_events_dict_for_room(room_id)

return await self.third_party_rules.check_room_can_be_added_to_public_rooms_directory(
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
room_id, state_events
)

async def _get_state_events_dict_for_room(
self, room_id: str
) -> Dict[Tuple[str, str], EventBase]:
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"""Given a room ID, return the state events of that room.

Args:
room_id: The ID of the room.

Returns:
A dict mapping (event type, state key) to state event.
"""
state_ids = await self.store.get_filtered_current_state_ids(room_id)
room_state_events = await self.store.get_events(state_ids.values())

state_events = {}
for key, event_id in state_ids.items():
state_events[key] = room_state_events[event_id]

ret = await self.third_party_rules.check_threepid_can_be_invited(
medium, address, state_events
)
return ret
return state_events
10 changes: 10 additions & 0 deletions synapse/handlers/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self, hs):
self.config = hs.config
self.enable_room_list_search = hs.config.enable_room_list_search
self.require_membership = hs.config.require_membership_for_aliases
self.third_party_event_rules = hs.get_third_party_event_rules()

self.federation = hs.get_federation_client()
hs.get_federation_registry().register_query_handler(
Expand Down Expand Up @@ -454,6 +455,15 @@ async def edit_published_room_list(
# per alias creation rule?
raise SynapseError(403, "Not allowed to publish room")

# Check if publishing is blocked by a third party module
allowed_by_third_party_rules = await (
self.third_party_event_rules.check_room_can_be_added_to_public_rooms_directory(
room_id
)
)
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
if not allowed_by_third_party_rules:
raise SynapseError(403, "Not allowed to publish room")

await self.store.set_room_is_public(room_id, making_public)

async def edit_published_appservice_room_list(
Expand Down
19 changes: 13 additions & 6 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,13 +683,20 @@ async def create_room(

directory_handler = self.hs.get_handlers().directory_handler
if room_alias:
await directory_handler.create_association(
requester=requester,
room_id=room_id,
room_alias=room_alias,
servers=[self.hs.hostname],
check_membership=False,
# Check if publishing is blocked by a third party module
allowed_by_third_party_rules = await (
self.third_party_event_rules.check_room_can_be_added_to_public_rooms_directory(
room_id
)
)
if allowed_by_third_party_rules:
await directory_handler.create_association(
requester=requester,
room_id=room_id,
room_alias=room_alias,
servers=[self.hs.hostname],
check_membership=False,
)
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved

if is_public:
if not self.config.is_publishing_room_allowed(user_id, room_id, room_alias):
Expand Down
19 changes: 19 additions & 0 deletions synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ async def get_public_room_ids(self) -> List[str]:
desc="get_public_room_ids",
)

async def get_room_is_public(self, room_id: str) -> bool:
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"""Returns whether a given room is public.

Args:
room_id: The ID of the room.

Returns:
Whether the room is public.
"""
row = await self.db_pool.simple_select_onecol(
table="rooms",
keyvalues={"room_id": room_id},
retcol="is_public",
desc="get_room_is_public",
)
if row:
return bool(row[0])
return False

async def count_public_rooms(
self,
network_tuple: Optional[ThirdPartyInstanceID],
Expand Down
66 changes: 54 additions & 12 deletions tests/rest/client/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,24 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from synapse.events.third_party_rules import PublicRoomsManager
from synapse.rest import admin
from synapse.rest.client.v1 import login, room
from synapse.types import Requester

from tests import unittest


class ThirdPartyRulesTestModule:
def __init__(self, config):
def __init__(self, config, *args, **kwargs):
pass

def check_event_allowed(self, event, context):
async def on_create_room(
self, requester: Requester, config: dict, is_requester_admin: bool
):
return True

async def check_event_allowed(self, event, context):
if event.type == "foo.bar.forbidden":
return False
else:
Expand Down Expand Up @@ -51,29 +57,65 @@ def make_homeserver(self, reactor, clock):
self.hs = self.setup_test_homeserver(config=config)
return self.hs

def prepare(self, reactor, clock, homeserver):
# Create a user and room to play with during the tests
self.user_id = self.register_user("kermit", "monkey")
self.tok = self.login("kermit", "monkey")

self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)

def test_third_party_rules(self):
"""Tests that a forbidden event is forbidden from being sent, but an allowed one
can be sent.
"""
user_id = self.register_user("kermit", "monkey")
tok = self.login("kermit", "monkey")

room_id = self.helper.create_room_as(user_id, tok=tok)

request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/foo.bar.allowed/1" % room_id,
"/_matrix/client/r0/rooms/%s/send/foo.bar.allowed/1" % self.room_id,
{},
access_token=tok,
access_token=self.tok,
)
self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)

request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/foo.bar.forbidden/1" % room_id,
"/_matrix/client/r0/rooms/%s/send/foo.bar.forbidden/1" % self.room_id,
{},
access_token=tok,
access_token=self.tok,
)
self.render(request)
self.assertEquals(channel.result["code"], b"403", channel.result)

def test_public_rooms_manager(self):
"""Tests that a room can be added and removed from the public rooms list,
as well as have its public rooms directory state queried.
"""
public_rooms_manager = PublicRoomsManager(self.hs)

# The room should not currently be in the public rooms directory
is_in_public_rooms = self.get_success(
public_rooms_manager.room_is_in_public_directory(self.room_id)
)
self.assertFalse(is_in_public_rooms)

# Let's try adding it to the public rooms directory
self.get_success(
public_rooms_manager.add_room_to_public_directory(self.room_id)
)

# And checking whether it's in there...
is_in_public_rooms = self.get_success(
public_rooms_manager.room_is_in_public_directory(self.room_id)
)
self.assertTrue(is_in_public_rooms)

# Let's remove it again
self.get_success(
public_rooms_manager.remove_room_from_public_directory(self.room_id)
)

# Should be gone
is_in_public_rooms = self.get_success(
public_rooms_manager.room_is_in_public_directory(self.room_id)
)
self.assertFalse(is_in_public_rooms)