Merge pull request #5484 from matrix-org/babolivier/dinsic_access_rules
Implement custom access rulespull/5542/head
commit
fa4efb5967
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
import email.utils
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.types import get_domain_from_id
|
||||
|
||||
ACCESS_RULES_TYPE = "im.vector.room.access_rules"
|
||||
ACCESS_RULE_RESTRICTED = "restricted"
|
||||
ACCESS_RULE_UNRESTRICTED = "unrestricted"
|
||||
ACCESS_RULE_DIRECT = "direct"
|
||||
|
||||
VALID_ACCESS_RULES = (
|
||||
ACCESS_RULE_DIRECT,
|
||||
ACCESS_RULE_RESTRICTED,
|
||||
ACCESS_RULE_UNRESTRICTED,
|
||||
)
|
||||
|
||||
|
||||
class RoomAccessRules(object):
|
||||
"""Implementation of the ThirdPartyEventRules module API that allows federation admins
|
||||
to define custom rules for specific events and actions.
|
||||
Implements the custom behaviour for the "im.vector.room.access_rules" state event.
|
||||
|
||||
Takes a config in the format:
|
||||
|
||||
third_party_event_rules:
|
||||
module: third_party_rules.RoomAccessRules
|
||||
config:
|
||||
# List of domains (server names) that can't be invited to rooms if the
|
||||
# "restricted" rule is set. Defaults to an empty list.
|
||||
domains_forbidden_when_restricted: []
|
||||
|
||||
# Identity server to use when checking the HS an email address belongs to
|
||||
# using the /info endpoint. Required.
|
||||
id_server: "vector.im"
|
||||
|
||||
Don't forget to consider if you can invite users from your own domain.
|
||||
"""
|
||||
|
||||
def __init__(self, config, http_client):
|
||||
self.http_client = http_client
|
||||
|
||||
self.id_server = config["id_server"]
|
||||
|
||||
self.domains_forbidden_when_restricted = config.get(
|
||||
"domains_forbidden_when_restricted", [],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config):
|
||||
if "id_server" in config:
|
||||
return config
|
||||
else:
|
||||
raise ConfigError("No IS for event rules TchapEventRules")
|
||||
|
||||
def on_create_room(self, requester, config, is_requester_admin):
|
||||
"""Implements synapse.events.ThirdPartyEventRules.on_create_room
|
||||
|
||||
Checks if a im.vector.room.access_rules event is being set during room creation.
|
||||
If yes, make sure the event is correct. Otherwise, append an event with the
|
||||
default rule to the initial state.
|
||||
"""
|
||||
is_direct = config.get("is_direct")
|
||||
rules_in_initial_state = False
|
||||
|
||||
# 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",)
|
||||
|
||||
# Make sure the rule name is valid.
|
||||
if rule not in VALID_ACCESS_RULES:
|
||||
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",)
|
||||
|
||||
# If there's no rules event in the initial state, create one with the default
|
||||
# setting.
|
||||
if not rules_in_initial_state:
|
||||
if is_direct:
|
||||
default_rule = ACCESS_RULE_DIRECT
|
||||
else:
|
||||
default_rule = ACCESS_RULE_RESTRICTED
|
||||
|
||||
if not config.get("initial_state"):
|
||||
config["initial_state"] = []
|
||||
|
||||
config["initial_state"].append({
|
||||
"type": ACCESS_RULES_TYPE,
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"rule": default_rule,
|
||||
}
|
||||
})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_threepid_can_be_invited(self, medium, address, state_events):
|
||||
"""Implements synapse.events.ThirdPartyEventRules.check_threepid_can_be_invited
|
||||
|
||||
Check if a threepid can be invited to the room via a 3PID invite given the current
|
||||
rules and the threepid's address, by retrieving the HS it's mapped to from the
|
||||
configured identity server, and checking if we can invite users from it.
|
||||
"""
|
||||
rule = self._get_rule_from_state(state_events)
|
||||
|
||||
if medium != "email":
|
||||
defer.returnValue(False)
|
||||
|
||||
if rule != ACCESS_RULE_RESTRICTED:
|
||||
# Only "restricted" requires filtering 3PID invites. We don't need to do
|
||||
# anything for "direct" here, because only "restricted" requires filtering
|
||||
# based on the HS the address is mapped to.
|
||||
defer.returnValue(True)
|
||||
|
||||
parsed_address = email.utils.parseaddr(address)[1]
|
||||
if parsed_address != address:
|
||||
# Avoid reproducing the security issue described here:
|
||||
# https://matrix.org/blog/2019/04/18/security-update-sydent-1-0-2
|
||||
# It's probably not worth it but let's just be overly safe here.
|
||||
defer.returnValue(False)
|
||||
|
||||
# Get the HS this address belongs to from the identity server.
|
||||
res = yield self.http_client.get_json(
|
||||
"https://%s/_matrix/identity/api/v1/info" % (self.id_server,),
|
||||
{
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
}
|
||||
)
|
||||
|
||||
# Look for a domain that's not forbidden from being invited.
|
||||
if not res.get("hs"):
|
||||
defer.returnValue(False)
|
||||
if res.get("hs") in self.domains_forbidden_when_restricted:
|
||||
defer.returnValue(False)
|
||||
|
||||
defer.returnValue(True)
|
||||
|
||||
def check_event_allowed(self, event, state_events):
|
||||
"""Implements synapse.events.ThirdPartyEventRules.check_event_allowed
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
return ret
|
||||
|
||||
def _on_rules_change(self, event, state_events):
|
||||
"""Implement the checks and behaviour specified on allowing or forbidding a new
|
||||
im.vector.room.access_rules event.
|
||||
|
||||
Args:
|
||||
event (synapse.events.EventBase): The event to check.
|
||||
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.
|
||||
"""
|
||||
new_rule = event.content.get("rule")
|
||||
|
||||
# Check for invalid values.
|
||||
if new_rule not in VALID_ACCESS_RULES:
|
||||
return False
|
||||
|
||||
# Make sure we don't apply "direct" if the room has more than two members.
|
||||
if new_rule == ACCESS_RULE_DIRECT:
|
||||
existing_members, threepid_tokens = self._get_members_and_tokens_from_state(
|
||||
state_events,
|
||||
)
|
||||
|
||||
if len(existing_members) > 2 or len(threepid_tokens) > 1:
|
||||
return False
|
||||
|
||||
prev_rules_event = state_events.get((ACCESS_RULES_TYPE, ""))
|
||||
|
||||
# Now that we know the new rule doesn't break the "direct" case, we can allow any
|
||||
# new rule in rooms that had none before.
|
||||
if prev_rules_event is None:
|
||||
return True
|
||||
|
||||
prev_rule = prev_rules_event.content.get("rule")
|
||||
|
||||
# Currently, we can only go from "restricted" to "unrestricted".
|
||||
if prev_rule == ACCESS_RULE_RESTRICTED and new_rule == ACCESS_RULE_UNRESTRICTED:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _apply_restricted(self, event):
|
||||
"""Implements the checks and behaviour specified for the "restricted" rule.
|
||||
|
||||
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:
|
||||
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):
|
||||
"""Implements the checks and behaviour specified for the "unrestricted" rule.
|
||||
|
||||
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):
|
||||
"""Implements the checks and behaviour specified for the "direct" rule.
|
||||
|
||||
Args:
|
||||
event (synapse.events.EventBase): The event to check.
|
||||
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.
|
||||
"""
|
||||
# "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,
|
||||
)
|
||||
|
||||
# There should never be more than one 3PID invite in the room state: if the second
|
||||
# original user came and left, and we're inviting them using their email address,
|
||||
# given we know they have a Matrix account binded to the address (so they could
|
||||
# join the first time), Synapse will successfully look it up before attempting to
|
||||
# store an invite on the IS.
|
||||
if len(threepid_tokens) == 1 and event.type == EventTypes.ThirdPartyInvite:
|
||||
# If we already have a 3PID invite in flight, don't accept another one.
|
||||
return False
|
||||
|
||||
if len(existing_members) == 2:
|
||||
# If the user was within the two initial user of the room, Synapse would have
|
||||
# looked it up successfully and thus sent a m.room.member here instead of
|
||||
# m.room.third_party_invite.
|
||||
if event.type == EventTypes.ThirdPartyInvite:
|
||||
return False
|
||||
|
||||
# We can only have m.room.member events here. The rule in this case is to only
|
||||
# allow the event if its target is one of the initial two members in the room,
|
||||
# i.e. the state key of one of the two m.room.member states in the room.
|
||||
return event.state_key in existing_members
|
||||
|
||||
# We're alone in the room (and always have been) and there's one 3PID invite in
|
||||
# flight.
|
||||
if len(existing_members) == 1 and len(threepid_tokens) == 1:
|
||||
# We can only have m.room.member events here. In this case, we can only allow
|
||||
# the event if it's either a m.room.member from the joined user (we can assume
|
||||
# that the only m.room.member event is a join otherwise we wouldn't be able to
|
||||
# send an event to the room) or an an invite event which target is the invited
|
||||
# user.
|
||||
target = event.state_key
|
||||
is_from_threepid_invite = self._is_invite_from_threepid(
|
||||
event, threepid_tokens[0],
|
||||
)
|
||||
if is_from_threepid_invite or target == existing_members[0]:
|
||||
return True
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
state_events (dict[tuple[event type, state key], EventBase]): The set of state
|
||||
events.
|
||||
Returns:
|
||||
str, the name of the rule (either "direct", "restricted" or "unrestricted")
|
||||
"""
|
||||
access_rules = state_events.get((ACCESS_RULES_TYPE, ""))
|
||||
if access_rules is None:
|
||||
rule = ACCESS_RULE_RESTRICTED
|
||||
else:
|
||||
rule = access_rules.content.get("rule")
|
||||
return rule
|
||||
|
||||
@staticmethod
|
||||
def _get_members_and_tokens_from_state(state_events):
|
||||
"""Retrieves from a list of state events the list of users that have a
|
||||
m.room.member event in the room, and the tokens of 3PID invites in the room.
|
||||
|
||||
Args:
|
||||
state_events (dict[tuple[event type, state key], EventBase]): The set of state
|
||||
events.
|
||||
Returns:
|
||||
existing_members (list[str]): List of targets of the m.room.member events in
|
||||
the state.
|
||||
threepid_invite_tokens (list[str]): List of tokens of the 3PID invites in the
|
||||
state.
|
||||
"""
|
||||
existing_members = []
|
||||
threepid_invite_tokens = []
|
||||
for key, event in state_events.items():
|
||||
if key[0] == EventTypes.Member:
|
||||
existing_members.append(event.state_key)
|
||||
if key[0] == EventTypes.ThirdPartyInvite:
|
||||
threepid_invite_tokens.append(event.state_key)
|
||||
|
||||
return existing_members, threepid_invite_tokens
|
||||
|
||||
@staticmethod
|
||||
def _is_invite_from_threepid(invite, threepid_invite_token):
|
||||
"""Checks whether the given invite follows the given 3PID invite.
|
||||
|
||||
Args:
|
||||
invite (EventBase): The m.room.member event with "invite" membership.
|
||||
threepid_invite_token (str): The state key from the 3PID invite.
|
||||
"""
|
||||
token = invite.content.get("third_party_signed", {}).get("token", "")
|
||||
return token == threepid_invite_token
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 New Vector Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
|
||||
from mock import Mock
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.third_party_rules.access_rules import (
|
||||
ACCESS_RULE_DIRECT,
|
||||
ACCESS_RULE_RESTRICTED,
|
||||
ACCESS_RULE_UNRESTRICTED,
|
||||
ACCESS_RULES_TYPE,
|
||||
)
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class RoomAccessTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
config = self.default_config()
|
||||
|
||||
config["third_party_event_rules"] = {
|
||||
"module": "synapse.third_party_rules.access_rules.RoomAccessRules",
|
||||
"config": {
|
||||
"domains_forbidden_when_restricted": [
|
||||
"forbidden_domain"
|
||||
],
|
||||
"id_server": "testis",
|
||||
}
|
||||
}
|
||||
config["trusted_third_party_id_servers"] = [
|
||||
"testis",
|
||||
]
|
||||
|
||||
def send_invite(destination, room_id, event_id, pdu):
|
||||
return defer.succeed(pdu)
|
||||
|
||||
def get_json(uri, args={}, headers=None):
|
||||
address_domain = args["address"].split("@")[1]
|
||||
return defer.succeed({"hs": address_domain})
|
||||
|
||||
def post_urlencoded_get_json(uri, args={}, headers=None):
|
||||
token = ''.join(random.choice(string.ascii_letters) for _ in range(10))
|
||||
return defer.succeed({
|
||||
"token": token,
|
||||
"public_keys": [
|
||||
{
|
||||
"public_key": "serverpublickey",
|
||||
"key_validity_url": "https://testis/pubkey/isvalid",
|
||||
},
|
||||
{
|
||||
"public_key": "phemeralpublickey",
|
||||
"key_validity_url": "https://testis/pubkey/ephemeral/isvalid",
|
||||
},
|
||||
],
|
||||
"display_name": "f...@b...",
|
||||
})
|
||||
|
||||
mock_federation_client = Mock(spec=[
|
||||
"send_invite",
|
||||
])
|
||||
mock_federation_client.send_invite.side_effect = send_invite
|
||||
|
||||
mock_http_client = Mock(spec=[
|
||||
"get_json",
|
||||
"post_urlencoded_get_json"
|
||||
])
|
||||
# Mocking the response for /info on the IS API.
|
||||
mock_http_client.get_json.side_effect = get_json
|
||||
# Mocking the response for /store-invite on the IS API.
|
||||
mock_http_client.post_urlencoded_get_json.side_effect = post_urlencoded_get_json
|
||||
self.hs = self.setup_test_homeserver(
|
||||
config=config,
|
||||
federation_client=mock_federation_client,
|
||||
simple_http_client=mock_http_client,
|
||||
)
|
||||
|
||||
return self.hs
|
||||
|
||||
def prepare(self, reactor, clock, homeserver):
|
||||
self.user_id = self.register_user("kermit", "monkey")
|
||||
self.tok = self.login("kermit", "monkey")
|
||||
|
||||
self.restricted_room = self.create_room()
|
||||
self.unrestricted_room = self.create_room(rule=ACCESS_RULE_UNRESTRICTED)
|
||||
self.direct_rooms = [
|
||||
self.create_room(direct=True),
|
||||
self.create_room(direct=True),
|
||||
self.create_room(direct=True),
|
||||
]
|
||||
|
||||
self.invitee_id = self.register_user("invitee", "test")
|
||||
self.invitee_tok = self.login("invitee", "test")
|
||||
|
||||
self.helper.invite(
|
||||
room=self.direct_rooms[0],
|
||||
src=self.user_id,
|
||||
targ=self.invitee_id,
|
||||
tok=self.tok,
|
||||
)
|
||||
|
||||
def test_create_room_no_rule(self):
|
||||
"""Tests that creating a room with no rule will set the default value."""
|
||||
room_id = self.create_room()
|
||||
rule = self.current_rule_in_room(room_id)
|
||||
|
||||
self.assertEqual(rule, ACCESS_RULE_RESTRICTED)
|
||||
|
||||
def test_create_room_direct_no_rule(self):
|
||||
"""Tests that creating a direct room with no rule will set the default value."""
|
||||
room_id = self.create_room(direct=True)
|
||||
rule = self.current_rule_in_room(room_id)
|
||||
|
||||
self.assertEqual(rule, ACCESS_RULE_DIRECT)
|
||||
|
||||
def test_create_room_valid_rule(self):
|
||||
"""Tests that creating a room with a valid rule will set the right value."""
|
||||
room_id = self.create_room(rule=ACCESS_RULE_UNRESTRICTED)
|
||||
rule = self.current_rule_in_room(room_id)
|
||||
|
||||
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."""
|
||||
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.
|
||||
"""
|
||||
self.create_room(direct=True, rule=ACCESS_RULE_RESTRICTED, expected_code=400)
|
||||
|
||||
def test_restricted(self):
|
||||
"""Tests that in restricted mode we're unable to invite users from blacklisted
|
||||
servers but can invite other users.
|
||||
"""
|
||||
# We can't invite a user from a forbidden HS.
|
||||
self.helper.invite(
|
||||
room=self.restricted_room,
|
||||
src=self.user_id,
|
||||
targ="@test:forbidden_domain",
|
||||
tok=self.tok,
|
||||
expect_code=403,
|
||||
)
|
||||
|
||||
# We can invite a user which HS isn't forbidden.
|
||||
self.helper.invite(
|
||||
room=self.restricted_room,
|
||||
src=self.user_id,
|
||||
targ="@test:allowed_domain",
|
||||
tok=self.tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
# We can't send a 3PID invite to an address that is mapped to a forbidden HS.
|
||||
self.send_threepid_invite(
|
||||
address="test@forbidden_domain",
|
||||
room_id=self.restricted_room,
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can send a 3PID invite to an address that is mapped to an HS that's not
|
||||
# forbidden.
|
||||
self.send_threepid_invite(
|
||||
address="test@allowed_domain",
|
||||
room_id=self.restricted_room,
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
def test_direct(self):
|
||||
"""Tests that, in direct mode, other users than the initial two can't be invited,
|
||||
but the following scenario works:
|
||||
* invited user joins the room
|
||||
* invited user leaves the room
|
||||
* room creator re-invites invited user
|
||||
Also tests that a user from a HS that's in the list of forbidden domains (to use
|
||||
in restricted mode) can be invited.
|
||||
"""
|
||||
not_invited_user = "@not_invited:forbidden_domain"
|
||||
|
||||
# We can't invite a new user to the room.
|
||||
self.helper.invite(
|
||||
room=self.direct_rooms[0],
|
||||
src=self.user_id,
|
||||
targ=not_invited_user,
|
||||
tok=self.tok,
|
||||
expect_code=403,
|
||||
)
|
||||
|
||||
# The invited user can join the room.
|
||||
self.helper.join(
|
||||
room=self.direct_rooms[0],
|
||||
user=self.invitee_id,
|
||||
tok=self.invitee_tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
# The invited user can leave the room.
|
||||
self.helper.leave(
|
||||
room=self.direct_rooms[0],
|
||||
user=self.invitee_id,
|
||||
tok=self.invitee_tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
# The invited user can be re-invited to the room.
|
||||
self.helper.invite(
|
||||
room=self.direct_rooms[0],
|
||||
src=self.user_id,
|
||||
targ=self.invitee_id,
|
||||
tok=self.tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
# If we're alone in the room and have always been the only member, we can invite
|
||||
# someone.
|
||||
self.helper.invite(
|
||||
room=self.direct_rooms[1],
|
||||
src=self.user_id,
|
||||
targ=not_invited_user,
|
||||
tok=self.tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
# We can't send a 3PID invite to a room that already has two members.
|
||||
self.send_threepid_invite(
|
||||
address="test@allowed_domain",
|
||||
room_id=self.direct_rooms[0],
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can't send a 3PID invite to a room that already has a pending invite.
|
||||
self.send_threepid_invite(
|
||||
address="test@allowed_domain",
|
||||
room_id=self.direct_rooms[1],
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can send a 3PID invite to a room in which we've always been the only member.
|
||||
self.send_threepid_invite(
|
||||
address="test@forbidden_domain",
|
||||
room_id=self.direct_rooms[2],
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
# We can send a 3PID invite to a room in which there's a 3PID invite.
|
||||
self.send_threepid_invite(
|
||||
address="test@forbidden_domain",
|
||||
room_id=self.direct_rooms[2],
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
def test_unrestricted(self):
|
||||
"""Tests that, in unrestricted mode, we can invite whoever we want.
|
||||
"""
|
||||
# We can invite
|
||||
self.helper.invite(
|
||||
room=self.unrestricted_room,
|
||||
src=self.user_id,
|
||||
targ="@test:forbidden_domain",
|
||||
tok=self.tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
self.helper.invite(
|
||||
room=self.unrestricted_room,
|
||||
src=self.user_id,
|
||||
targ="@test:not_forbidden_domain",
|
||||
tok=self.tok,
|
||||
expect_code=200,
|
||||
)
|
||||
|
||||
# We can send a 3PID invite to an address that is mapped to a forbidden HS.
|
||||
self.send_threepid_invite(
|
||||
address="test@forbidden_domain",
|
||||
room_id=self.unrestricted_room,
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
# We can send a 3PID invite to an address that is mapped to an HS that's not
|
||||
# forbidden.
|
||||
self.send_threepid_invite(
|
||||
address="test@allowed_domain",
|
||||
room_id=self.unrestricted_room,
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
def test_change_rules(self):
|
||||
"""Tests that we can only change the current rule from restricted to
|
||||
unrestricted.
|
||||
"""
|
||||
# We can change the rule from restricted to unrestricted.
|
||||
self.change_rule_in_room(
|
||||
room_id=self.restricted_room,
|
||||
new_rule=ACCESS_RULE_UNRESTRICTED,
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
# We can't change the rule from restricted to direct.
|
||||
self.change_rule_in_room(
|
||||
room_id=self.restricted_room,
|
||||
new_rule=ACCESS_RULE_DIRECT,
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can't change the rule from unrestricted to restricted.
|
||||
self.change_rule_in_room(
|
||||
room_id=self.unrestricted_room,
|
||||
new_rule=ACCESS_RULE_RESTRICTED,
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can't change the rule from unrestricted to direct.
|
||||
self.change_rule_in_room(
|
||||
room_id=self.unrestricted_room,
|
||||
new_rule=ACCESS_RULE_DIRECT,
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can't change the rule from direct to restricted.
|
||||
self.change_rule_in_room(
|
||||
room_id=self.direct_rooms[0],
|
||||
new_rule=ACCESS_RULE_RESTRICTED,
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
# We can't change the rule from direct to unrestricted.
|
||||
self.change_rule_in_room(
|
||||
room_id=self.direct_rooms[0],
|
||||
new_rule=ACCESS_RULE_UNRESTRICTED,
|
||||
expected_code=403,
|
||||
)
|
||||
|
||||
def create_room(self, direct=False, rule=None, expected_code=200):
|
||||
content = {
|
||||
"is_direct": direct,
|
||||
}
|
||||
|
||||
if rule:
|
||||
content["initial_state"] = [{
|
||||
"type": ACCESS_RULES_TYPE,
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"rule": rule,
|
||||
}
|
||||
}]
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/r0/createRoom",
|
||||
json.dumps(content),
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
if expected_code == 200:
|
||||
return channel.json_body["room_id"]
|
||||
|
||||
def current_rule_in_room(self, room_id):
|
||||
request, channel = self.make_request(
|
||||
"GET",
|
||||
"/_matrix/client/r0/rooms/%s/state/%s" % (room_id, ACCESS_RULES_TYPE),
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
return channel.json_body["rule"]
|
||||
|
||||
def change_rule_in_room(self, room_id, new_rule, expected_code=200):
|
||||
data = {
|
||||
"rule": new_rule,
|
||||
}
|
||||
request, channel = self.make_request(
|
||||
"PUT",
|
||||
"/_matrix/client/r0/rooms/%s/state/%s" % (room_id, ACCESS_RULES_TYPE),
|
||||
json.dumps(data),
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
def send_threepid_invite(self, address, room_id, expected_code=200):
|
||||
params = {
|
||||
"id_server": "testis",
|
||||
"medium": "email",
|
||||
"address": address,
|
||||
}
|
||||
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/r0/rooms/%s/invite" % room_id,
|
||||
json.dumps(params),
|
||||
access_token=self.tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
Loading…
Reference in New Issue