Merge pull request #5610 from matrix-org/babolivier/power-levels

Implement new restrictions on power levels
pull/5815/head dinsic_2019-07-04
Brendan Abolivier 2019-07-04 11:12:47 +01:00 committed by GitHub
commit 5fe0cea37e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 173 additions and 38 deletions

1
changelog.d/5610.feature Normal file
View File

@ -0,0 +1 @@
Implement new custom event rules for power levels.

View File

@ -33,6 +33,21 @@ VALID_ACCESS_RULES = (
ACCESS_RULE_UNRESTRICTED,
)
# Rules to which we need to apply the power levels restrictions.
#
# These are all of the rules that neither:
# * forbid users from joining based on a server blacklist (which means that there
# is no need to apply power level restrictions), nor
# * target direct chats (since we allow both users to be room admins in this case).
#
# The power-level restrictions, when they are applied, prevent the following:
# * the default power level for users (users_default) being set to anything other than 0.
# * a non-default power level being assigned to any user which would be forbidden from
# joining a restricted room.
RULES_WITH_RESTRICTED_POWER_LEVELS = (
ACCESS_RULE_UNRESTRICTED,
)
class RoomAccessRules(object):
"""Implementation of the ThirdPartyEventRules module API that allows federation admins
@ -79,34 +94,32 @@ class RoomAccessRules(object):
default rule to the initial state.
"""
is_direct = config.get("is_direct")
rules_in_initial_state = False
rule = None
# If there's a rules event in the initial state, check if it complies with the
# spec for im.vector.room.access_rules and deny the request if not.
for event in config.get("initial_state", []):
if event["type"] == ACCESS_RULES_TYPE:
rules_in_initial_state = True
rule = event["content"].get("rule")
# Make sure the event has a valid content.
if rule is None:
raise SynapseError(400, "Invalid access rule",)
raise SynapseError(400, "Invalid access rule")
# Make sure the rule name is valid.
if rule not in VALID_ACCESS_RULES:
raise SynapseError(400, "Invalid access rule", )
raise SynapseError(400, "Invalid access rule")
# Make sure the rule is "direct" if the room is a direct chat.
if (
(is_direct and rule != ACCESS_RULE_DIRECT)
or (rule == ACCESS_RULE_DIRECT and not is_direct)
):
raise SynapseError(400, "Invalid access rule",)
raise SynapseError(400, "Invalid access rule")
# If there's no rules event in the initial state, create one with the default
# setting.
if not rules_in_initial_state:
if not rule:
if is_direct:
default_rule = ACCESS_RULE_DIRECT
else:
@ -123,6 +136,22 @@ class RoomAccessRules(object):
}
})
rule = default_rule
# Check if the creator can override values for the power levels.
allowed = self._is_power_level_content_allowed(
config.get("power_level_content_override", {}), rule,
)
if not allowed:
raise SynapseError(400, "Invalid power levels content override")
# Second loop for events we need to know the current rule to process.
for event in config.get("initial_state", []):
if event["type"] == EventTypes.PowerLevels:
allowed = self._is_power_level_content_allowed(event["content"], rule)
if not allowed:
raise SynapseError(400, "Invalid power levels content")
@defer.inlineCallbacks
def check_threepid_can_be_invited(self, medium, address, state_events):
"""Implements synapse.events.ThirdPartyEventRules.check_threepid_can_be_invited
@ -172,24 +201,19 @@ class RoomAccessRules(object):
Checks the event's type and the current rule and calls the right function to
determine whether the event can be allowed.
"""
# Special-case the access rules event.
if event.type == ACCESS_RULES_TYPE:
return self._on_rules_change(event, state_events)
# We need to know the rule to apply when processing the event types below.
rule = self._get_rule_from_state(state_events)
if rule == ACCESS_RULE_RESTRICTED:
ret = self._apply_restricted(event)
elif rule == ACCESS_RULE_UNRESTRICTED:
ret = self._apply_unrestricted()
elif rule == ACCESS_RULE_DIRECT:
ret = self._apply_direct(event, state_events)
else:
# We currently apply the default (restricted) if we don't know the rule, we
# might want to change that in the future.
ret = self._apply_restricted(event)
if event.type == EventTypes.PowerLevels:
return self._is_power_level_content_allowed(event.content, rule)
return ret
if event.type == EventTypes.Member or event.type == EventTypes.ThirdPartyInvite:
return self._on_membership_or_invite(event, rule, state_events)
return True
def _on_rules_change(self, event, state_events):
"""Implement the checks and behaviour specified on allowing or forbidding a new
@ -232,35 +256,67 @@ class RoomAccessRules(object):
return False
def _apply_restricted(self, event):
def _on_membership_or_invite(self, event, rule, state_events):
"""Applies the correct rule for incoming m.room.member and
m.room.third_party_invite events.
Args:
event (synapse.events.EventBase): The event to check.
rule (str): The name of the rule to apply.
state_events (dict[tuple[event type, state key], EventBase]): The state of the
room before the event was sent.
Returns:
bool, True if the event can be allowed, False otherwise.
"""
if rule == ACCESS_RULE_RESTRICTED:
ret = self._on_membership_or_invite_restricted(event)
elif rule == ACCESS_RULE_UNRESTRICTED:
ret = self._on_membership_or_invite_unrestricted()
elif rule == ACCESS_RULE_DIRECT:
ret = self._on_membership_or_invite_direct(event, state_events)
else:
# We currently apply the default (restricted) if we don't know the rule, we
# might want to change that in the future.
ret = self._on_membership_or_invite_restricted(event)
return ret
def _on_membership_or_invite_restricted(self, event):
"""Implements the checks and behaviour specified for the "restricted" rule.
"restricted" currently means that users can only invite users if their server is
included in a limited list of domains.
Args:
event (synapse.events.EventBase): The event to check.
Returns:
bool, True if the event can be allowed, False otherwise.
"""
# "restricted" currently means that users can only invite users if their server is
# included in a limited list of domains.
# We're not filtering on m.room.third_party_member events here because the
# filtering on threepids is done in check_threepid_can_be_invited.
if event.type != EventTypes.Member:
# We're not applying the rules on m.room.third_party_member events here because
# the filtering on threepids is done in check_threepid_can_be_invited, which is
# called before check_event_allowed.
if event.type == EventTypes.ThirdPartyInvite:
return True
invitee_domain = get_domain_from_id(event.state_key)
return invitee_domain not in self.domains_forbidden_when_restricted
def _apply_unrestricted(self):
def _on_membership_or_invite_unrestricted(self):
"""Implements the checks and behaviour specified for the "unrestricted" rule.
"unrestricted" currently means that every event is allowed.
Returns:
bool, True if the event can be allowed, False otherwise.
"""
# "unrestricted" currently means that every event is allowed.
return True
def _apply_direct(self, event, state_events):
def _on_membership_or_invite_direct(self, event, state_events):
"""Implements the checks and behaviour specified for the "direct" rule.
"direct" currently means that no member is allowed apart from the two initial
members the room was created for (i.e. the room's creator and their first
invitee).
Args:
event (synapse.events.EventBase): The event to check.
state_events (dict[tuple[event type, state key], EventBase]): The state of the
@ -268,12 +324,6 @@ class RoomAccessRules(object):
Returns:
bool, True if the event can be allowed, False otherwise.
"""
# "direct" currently means that no member is allowed apart from the two initial
# members the room was created for (i.e. the room's creator and their first
# invitee).
if event.type != EventTypes.Member and event.type != EventTypes.ThirdPartyInvite:
return True
# Get the room memberships and 3PID invite tokens from the room's state.
existing_members, threepid_tokens = self._get_members_and_tokens_from_state(
state_events,
@ -319,6 +369,42 @@ class RoomAccessRules(object):
return True
def _is_power_level_content_allowed(self, content, access_rule):
"""Check if a given power levels event is permitted under the given access rule.
It shouldn't be allowed if it either changes the default PL to a non-0 value or
gives a non-0 PL to a user that would have been forbidden from joining the room
under a more restrictive access rule.
Args:
content (dict[]): The content of the m.room.power_levels event to check.
access_rule (str): The access rule in place in this room.
Returns:
bool, True if the event can be allowed, False otherwise.
"""
# Check if we need to apply the restrictions with the current rule.
if access_rule not in RULES_WITH_RESTRICTED_POWER_LEVELS:
return True
# If users_default is explicitly set to a non-0 value, deny the event.
users_default = content.get('users_default', 0)
if users_default:
return False
users = content.get('users', {})
for user_id, power_level in users.items():
server_name = get_domain_from_id(user_id)
# Check the domain against the blacklist. If found, and the PL isn't 0, deny
# the event.
if (
server_name in self.domains_forbidden_when_restricted
and power_level != 0
):
return False
return True
@staticmethod
def _get_rule_from_state(state_events):
"""Extract the rule to be applied from the given set of state events.

View File

@ -22,6 +22,7 @@ from mock import Mock
from twisted.internet import defer
from synapse.api.constants import EventTypes
from synapse.rest import admin
from synapse.rest.client.v1 import login, room
from synapse.third_party_rules.access_rules import (
@ -147,12 +148,11 @@ class RoomAccessTestCase(unittest.HomeserverTestCase):
self.assertEqual(rule, ACCESS_RULE_UNRESTRICTED)
def test_create_room_invalid_rule(self):
"""Tests that creating a room with an invalid rule will set the default value."""
"""Tests that creating a room with an invalid rule will set fail."""
self.create_room(rule=ACCESS_RULE_DIRECT, expected_code=400)
def test_create_room_direct_invalid_rule(self):
"""Tests that creating a direct room with an invalid rule will set the default
value.
"""Tests that creating a direct room with an invalid rule will fail.
"""
self.create_room(direct=True, rule=ACCESS_RULE_RESTRICTED, expected_code=400)
@ -277,7 +277,9 @@ class RoomAccessTestCase(unittest.HomeserverTestCase):
)
def test_unrestricted(self):
"""Tests that, in unrestricted mode, we can invite whoever we want.
"""Tests that, in unrestricted mode, we can invite whoever we want, but we can
only change the power level of users that wouldn't be forbidden in restricted
mode.
"""
# We can invite
self.helper.invite(
@ -311,6 +313,52 @@ class RoomAccessTestCase(unittest.HomeserverTestCase):
expected_code=200,
)
# We can send a power level event that doesn't redefine the default PL or set a
# non-default PL for a user that would be forbidden in restricted mode.
self.helper.send_state(
room_id=self.unrestricted_room,
event_type=EventTypes.PowerLevels,
body={
"users": {
self.user_id: 100,
"@test:not_forbidden_domain": 10,
},
},
tok=self.tok,
expect_code=200,
)
# We can't send a power level event that redefines the default PL and doesn't set
# a non-default PL for a user that would be forbidden in restricted mode.
self.helper.send_state(
room_id=self.unrestricted_room,
event_type=EventTypes.PowerLevels,
body={
"users": {
self.user_id: 100,
"@test:not_forbidden_domain": 10,
},
"users_default": 10,
},
tok=self.tok,
expect_code=403,
)
# We can't send a power level event that doesn't redefines the default PL but sets
# a non-default PL for a user that would be forbidden in restricted mode.
self.helper.send_state(
room_id=self.unrestricted_room,
event_type=EventTypes.PowerLevels,
body={
"users": {
self.user_id: 100,
"@test:forbidden_domain": 10,
},
},
tok=self.tok,
expect_code=403,
)
def test_change_rules(self):
"""Tests that we can only change the current rule from restricted to
unrestricted.