Merge pull request #5610 from matrix-org/babolivier/power-levels
Implement new restrictions on power levelspull/5815/head dinsic_2019-07-04
commit
5fe0cea37e
|
|
@ -0,0 +1 @@
|
|||
Implement new custom event rules for power levels.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue