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

Fix checking whether a room can be published on creation. #11392

Merged
merged 8 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/11392.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug introduced in v1.13.0 where creating and publishing a room could cause errors if `room_list_publication_rules` is configured.
50 changes: 28 additions & 22 deletions synapse/config/room_directory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2018 New Vector Ltd
# Copyright 2021 Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -12,6 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List

from synapse.types import JsonDict
from synapse.util import glob_to_regex

from ._base import Config, ConfigError
Expand All @@ -20,7 +24,7 @@
class RoomDirectoryConfig(Config):
section = "roomdirectory"

def read_config(self, config, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

config? kwargs maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a separate branch which does all of the read_config and generate_config_section methods at once. Hope that's OK!

Copy link
Contributor

Choose a reason for hiding this comment

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

aye that's reet :)

def read_config(self, config, **kwargs) -> None:
self.enable_room_list_search = config.get("enable_room_list_search", True)

alias_creation_rules = config.get("alias_creation_rules")
Expand All @@ -47,7 +51,7 @@ def read_config(self, config, **kwargs):
_RoomDirectoryRule("room_list_publication_rules", {"action": "allow"})
]

def generate_config_section(self, config_dir_path, server_name, **kwargs):
def generate_config_section(self, config_dir_path, server_name, **kwargs) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def generate_config_section(self, config_dir_path, server_name, **kwargs) -> str:
def generate_config_section(self, config_dir_path: str, server_name: str, **kwargs) -> str:

not sure about the kwargs but is that right?

return """
# Uncomment to disable searching the public room list. When disabled
# blocks searching local and remote room lists for local and remote
Expand Down Expand Up @@ -113,33 +117,35 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
# action: allow
"""

def is_alias_creation_allowed(self, user_id, room_id, alias):
def is_alias_creation_allowed(self, user_id: str, room_id: str, alias: str) -> bool:
"""Checks if the given user is allowed to create the given alias

Args:
user_id (str)
room_id (str)
alias (str)
user_id: The user to check.
room_id: The room ID for the alias.
alias: The alias being created.

Returns:
boolean: True if user is allowed to create the alias
True if user is allowed to create the alias
"""
for rule in self._alias_creation_rules:
if rule.matches(user_id, room_id, [alias]):
return rule.action == "allow"

return False

def is_publishing_room_allowed(self, user_id, room_id, aliases):
def is_publishing_room_allowed(
self, user_id: str, room_id: str, aliases: List[str]
) -> bool:
"""Checks if the given user is allowed to publish the room

Args:
user_id (str)
room_id (str)
aliases (list[str]): any local aliases associated with the room
user_id: The user ID publishing the room.
room_id: The room being published.
aliases: any local aliases associated with the room

Returns:
boolean: True if user can publish room
True if user can publish room
"""
for rule in self._room_list_publication_rules:
if rule.matches(user_id, room_id, aliases):
Expand All @@ -153,11 +159,11 @@ class _RoomDirectoryRule:
creating an alias or publishing a room.
"""

def __init__(self, option_name, rule):
def __init__(self, option_name: str, rule: JsonDict):
"""
Args:
option_name (str): Name of the config option this rule belongs to
rule (dict): The rule as specified in the config
option_name: Name of the config option this rule belongs to
rule: The rule as specified in the config
"""

action = rule["action"]
Expand All @@ -181,18 +187,18 @@ def __init__(self, option_name, rule):
except Exception as e:
raise ConfigError("Failed to parse glob into regex") from e

def matches(self, user_id, room_id, aliases):
def matches(self, user_id: str, room_id: str, aliases: List[str]) -> bool:
"""Tests if this rule matches the given user_id, room_id and aliases.

Args:
user_id (str)
room_id (str)
aliases (list[str]): The associated aliases to the room. Will be a
single element for testing alias creation, and can be empty for
testing room publishing.
user_id: The user ID to check.
room_id: The room ID to check.
aliases: The associated aliases to the room. Will be a single element
for testing alias creation, and can be empty for testing room
publishing.

Returns:
boolean
True if the rule matches.
"""

# Note: The regexes are anchored at both ends
Expand Down
5 changes: 4 additions & 1 deletion synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,11 @@ async def create_room(
raise SynapseError(403, "Room visibility value not allowed.")

if is_public:
room_aliases = []
if room_alias:
room_aliases.append(room_alias.to_string())
if not self.config.roomdirectory.is_publishing_room_allowed(
user_id, room_id, room_alias
user_id, room_id, room_aliases
):
# Let's just return a generic message, as there may be all sorts of
# reasons why we said no. TODO: Allow configurable error messages
Expand Down
95 changes: 62 additions & 33 deletions tests/handlers/test_directory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2014-2016 OpenMarket Ltd
# Copyright 2021 Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -12,13 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.


from unittest.mock import Mock

import synapse.api.errors
import synapse.rest.admin
from synapse.api.constants import EventTypes
from synapse.config.room_directory import RoomDirectoryConfig
from synapse.rest.client import directory, login, room
from synapse.types import RoomAlias, create_requester

Expand Down Expand Up @@ -394,30 +393,23 @@ class TestCreateAliasACL(unittest.HomeserverTestCase):

servlets = [directory.register_servlets, room.register_servlets]

def prepare(self, reactor, clock, hs):
# We cheekily override the config to add custom alias creation rules
config = {}
def default_config(self):
config = super().default_config()

# Add custom alias creation rules to the config.
config["alias_creation_rules"] = [
{"user_id": "*", "alias": "#unofficial_*", "action": "allow"}
]
config["room_list_publication_rules"] = []

rd_config = RoomDirectoryConfig()
rd_config.read_config(config)

self.hs.config.roomdirectory.is_alias_creation_allowed = (
rd_config.is_alias_creation_allowed
)

return hs
return config

def test_denied(self):
room_id = self.helper.create_room_as(self.user_id)

channel = self.make_request(
"PUT",
b"directory/room/%23test%3Atest",
('{"room_id":"%s"}' % (room_id,)).encode("ascii"),
{"room_id": room_id},
)
self.assertEquals(403, channel.code, channel.result)

Expand All @@ -427,14 +419,35 @@ def test_allowed(self):
channel = self.make_request(
"PUT",
b"directory/room/%23unofficial_test%3Atest",
('{"room_id":"%s"}' % (room_id,)).encode("ascii"),
{"room_id": room_id},
)
self.assertEquals(200, channel.code, channel.result)

def test_denied_during_creation(self):
"""A room alias that is not allowed should be rejected during creation."""
# Invalid room alias.
self.helper.create_room_as(
self.user_id,
expect_code=403,
extra_content={"room_alias_name": "foo"},
)

class TestCreatePublishedRoomACL(unittest.HomeserverTestCase):
data = {"room_alias_name": "unofficial_test"}
def test_allowed_during_creation(self):
"""A valid room alias should be allowed during creation."""
room_id = self.helper.create_room_as(
self.user_id,
extra_content={"room_alias_name": "unofficial_test"},
)

channel = self.make_request(
"GET",
b"directory/room/%23unofficial_test%3Atest",
)
self.assertEquals(200, channel.code, channel.result)
self.assertEquals(channel.json_body["room_id"], room_id)


class TestCreatePublishedRoomACL(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
Expand All @@ -443,27 +456,30 @@ class TestCreatePublishedRoomACL(unittest.HomeserverTestCase):
]
hijack_auth = False

def prepare(self, reactor, clock, hs):
self.allowed_user_id = self.register_user("allowed", "pass")
self.allowed_access_token = self.login("allowed", "pass")
data = {"room_alias_name": "unofficial_test"}
allowed_localpart = "allowed"

self.denied_user_id = self.register_user("denied", "pass")
self.denied_access_token = self.login("denied", "pass")
def default_config(self):
config = super().default_config()

# This time we add custom room list publication rules
config = {}
config["alias_creation_rules"] = []
# Add custom room list publication rules to the config.
config["room_list_publication_rules"] = [
{
"user_id": "@" + self.allowed_localpart + "*",
"alias": "#unofficial_*",
"action": "allow",
},
{"user_id": "*", "alias": "*", "action": "deny"},
{"user_id": self.allowed_user_id, "alias": "*", "action": "allow"},
]

rd_config = RoomDirectoryConfig()
rd_config.read_config(config)
return config

self.hs.config.roomdirectory.is_publishing_room_allowed = (
rd_config.is_publishing_room_allowed
)
def prepare(self, reactor, clock, hs):
self.allowed_user_id = self.register_user(self.allowed_localpart, "pass")
self.allowed_access_token = self.login(self.allowed_localpart, "pass")

self.denied_user_id = self.register_user("denied", "pass")
self.denied_access_token = self.login("denied", "pass")

return hs

Expand Down Expand Up @@ -505,10 +521,23 @@ def test_allowed_with_publication_permission(self):
self.allowed_user_id,
tok=self.allowed_access_token,
extra_content=self.data,
is_public=False,
is_public=True,
expect_code=200,
)

def test_denied_publication_with_invalid_alias(self):
"""
Try to create a room, register an alias for it, and publish it,
as a user WITH permission to publish rooms.
"""
self.helper.create_room_as(
self.allowed_user_id,
tok=self.allowed_access_token,
extra_content={"room_alias_name": "foo"},
is_public=True,
expect_code=403,
)

def test_can_create_as_private_room_after_rejection(self):
"""
After failing to publish a room with an alias as a user without publish permission,
Expand Down