Merge pull request #689 from matrix-org/erikj/member
Do checks for memberships before creating events
This commit is contained in:
commit
bf14883a04
|
@ -204,20 +204,25 @@ class BaseHandler(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _create_new_client_event(self, builder):
|
def _create_new_client_event(self, builder, prev_event_ids=None):
|
||||||
latest_ret = yield self.store.get_latest_event_ids_and_hashes_in_room(
|
if prev_event_ids:
|
||||||
builder.room_id,
|
prev_events = yield self.store.add_event_hashes(prev_event_ids)
|
||||||
)
|
prev_max_depth = yield self.store.get_max_depth_of_events(prev_event_ids)
|
||||||
|
depth = prev_max_depth + 1
|
||||||
if latest_ret:
|
|
||||||
depth = max([d for _, _, d in latest_ret]) + 1
|
|
||||||
else:
|
else:
|
||||||
depth = 1
|
latest_ret = yield self.store.get_latest_event_ids_and_hashes_in_room(
|
||||||
|
builder.room_id,
|
||||||
|
)
|
||||||
|
|
||||||
prev_events = [
|
if latest_ret:
|
||||||
(event_id, prev_hashes)
|
depth = max([d for _, _, d in latest_ret]) + 1
|
||||||
for event_id, prev_hashes, _ in latest_ret
|
else:
|
||||||
]
|
depth = 1
|
||||||
|
|
||||||
|
prev_events = [
|
||||||
|
(event_id, prev_hashes)
|
||||||
|
for event_id, prev_hashes, _ in latest_ret
|
||||||
|
]
|
||||||
|
|
||||||
builder.prev_events = prev_events
|
builder.prev_events = prev_events
|
||||||
builder.depth = depth
|
builder.depth = depth
|
||||||
|
@ -226,49 +231,6 @@ class BaseHandler(object):
|
||||||
|
|
||||||
context = yield state_handler.compute_event_context(builder)
|
context = yield state_handler.compute_event_context(builder)
|
||||||
|
|
||||||
# If we've received an invite over federation, there are no latest
|
|
||||||
# events in the room, because we don't know enough about the graph
|
|
||||||
# fragment we received to treat it like a graph, so the above returned
|
|
||||||
# no relevant events. It may have returned some events (if we have
|
|
||||||
# joined and left the room), but not useful ones, like the invite.
|
|
||||||
if (
|
|
||||||
not self.is_host_in_room(context.current_state) and
|
|
||||||
builder.type == EventTypes.Member
|
|
||||||
):
|
|
||||||
prev_member_event = yield self.store.get_room_member(
|
|
||||||
builder.sender, builder.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# The prev_member_event may already be in context.current_state,
|
|
||||||
# despite us not being present in the room; in particular, if
|
|
||||||
# inviting user, and all other local users, have already left.
|
|
||||||
#
|
|
||||||
# In that case, we have all the information we need, and we don't
|
|
||||||
# want to drop "context" - not least because we may need to handle
|
|
||||||
# the invite locally, which will require us to have the whole
|
|
||||||
# context (not just prev_member_event) to auth it.
|
|
||||||
#
|
|
||||||
context_event_ids = (
|
|
||||||
e.event_id for e in context.current_state.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
prev_member_event and
|
|
||||||
prev_member_event.event_id not in context_event_ids
|
|
||||||
):
|
|
||||||
# The prev_member_event is missing from context, so it must
|
|
||||||
# have arrived over federation and is an outlier. We forcibly
|
|
||||||
# set our context to the invite we received over federation
|
|
||||||
builder.prev_events = (
|
|
||||||
prev_member_event.event_id,
|
|
||||||
prev_member_event.prev_events
|
|
||||||
)
|
|
||||||
|
|
||||||
context = yield state_handler.compute_event_context(
|
|
||||||
builder,
|
|
||||||
old_state=(prev_member_event,)
|
|
||||||
)
|
|
||||||
|
|
||||||
if builder.is_state():
|
if builder.is_state():
|
||||||
builder.prev_state = yield self.store.add_event_hashes(
|
builder.prev_state = yield self.store.add_event_hashes(
|
||||||
context.prev_state_events
|
context.prev_state_events
|
||||||
|
|
|
@ -176,7 +176,7 @@ class MessageHandler(BaseHandler):
|
||||||
defer.returnValue(chunk)
|
defer.returnValue(chunk)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def create_event(self, event_dict, token_id=None, txn_id=None):
|
def create_event(self, event_dict, token_id=None, txn_id=None, prev_event_ids=None):
|
||||||
"""
|
"""
|
||||||
Given a dict from a client, create a new event.
|
Given a dict from a client, create a new event.
|
||||||
|
|
||||||
|
@ -187,6 +187,9 @@ class MessageHandler(BaseHandler):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event_dict (dict): An entire event
|
event_dict (dict): An entire event
|
||||||
|
token_id (str)
|
||||||
|
txn_id (str)
|
||||||
|
prev_event_ids (list): The prev event ids to use when creating the event
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of created event (FrozenEvent), Context
|
Tuple of created event (FrozenEvent), Context
|
||||||
|
@ -225,6 +228,7 @@ class MessageHandler(BaseHandler):
|
||||||
|
|
||||||
event, context = yield self._create_new_client_event(
|
event, context = yield self._create_new_client_event(
|
||||||
builder=builder,
|
builder=builder,
|
||||||
|
prev_event_ids=prev_event_ids,
|
||||||
)
|
)
|
||||||
defer.returnValue((event, context))
|
defer.returnValue((event, context))
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,82 @@ class RoomMemberHandler(BaseHandler):
|
||||||
if remotedomains is not None:
|
if remotedomains is not None:
|
||||||
remotedomains.add(member.domain)
|
remotedomains.add(member.domain)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _local_membership_update(
|
||||||
|
self, requester, target, room_id, membership,
|
||||||
|
prev_event_ids,
|
||||||
|
txn_id=None,
|
||||||
|
ratelimit=True,
|
||||||
|
):
|
||||||
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
|
|
||||||
|
content = {"membership": membership}
|
||||||
|
if requester.is_guest:
|
||||||
|
content["kind"] = "guest"
|
||||||
|
|
||||||
|
event, context = yield msg_handler.create_event(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": content,
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": requester.user.to_string(),
|
||||||
|
"state_key": target.to_string(),
|
||||||
|
|
||||||
|
# For backwards compatibility:
|
||||||
|
"membership": membership,
|
||||||
|
},
|
||||||
|
token_id=requester.access_token_id,
|
||||||
|
txn_id=txn_id,
|
||||||
|
prev_event_ids=prev_event_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.handle_new_client_event(
|
||||||
|
requester,
|
||||||
|
event,
|
||||||
|
context,
|
||||||
|
extra_users=[target],
|
||||||
|
ratelimit=ratelimit,
|
||||||
|
)
|
||||||
|
|
||||||
|
prev_member_event = context.current_state.get(
|
||||||
|
(EventTypes.Member, target.to_string()),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.membership == Membership.JOIN:
|
||||||
|
if not prev_member_event or prev_member_event.membership != Membership.JOIN:
|
||||||
|
# Only fire user_joined_room if the user has acutally joined the
|
||||||
|
# room. Don't bother if the user is just changing their profile
|
||||||
|
# info.
|
||||||
|
yield user_joined_room(self.distributor, target, room_id)
|
||||||
|
elif event.membership == Membership.LEAVE:
|
||||||
|
if prev_member_event and prev_member_event.membership == Membership.JOIN:
|
||||||
|
user_left_room(self.distributor, target, room_id)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def remote_join(self, remote_room_hosts, room_id, user, content):
|
||||||
|
if len(remote_room_hosts) == 0:
|
||||||
|
raise SynapseError(404, "No known servers")
|
||||||
|
|
||||||
|
# We don't do an auth check if we are doing an invite
|
||||||
|
# join dance for now, since we're kinda implicitly checking
|
||||||
|
# that we are allowed to join when we decide whether or not we
|
||||||
|
# need to do the invite/join dance.
|
||||||
|
yield self.hs.get_handlers().federation_handler.do_invite_join(
|
||||||
|
remote_room_hosts,
|
||||||
|
room_id,
|
||||||
|
user.to_string(),
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
yield user_joined_room(self.distributor, user, room_id)
|
||||||
|
|
||||||
|
def reject_remote_invite(self, user_id, room_id, remote_room_hosts):
|
||||||
|
return self.hs.get_handlers().federation_handler.do_remotely_reject_invite(
|
||||||
|
remote_room_hosts,
|
||||||
|
room_id,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def update_membership(
|
def update_membership(
|
||||||
self,
|
self,
|
||||||
|
@ -120,28 +196,15 @@ class RoomMemberHandler(BaseHandler):
|
||||||
third_party_signed,
|
third_party_signed,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg_handler = self.hs.get_handlers().message_handler
|
if not remote_room_hosts:
|
||||||
|
remote_room_hosts = []
|
||||||
|
|
||||||
content = {"membership": effective_membership_state}
|
latest_event_ids = yield self.store.get_latest_event_ids_in_room(room_id)
|
||||||
if requester.is_guest:
|
current_state = yield self.state_handler.get_current_state(
|
||||||
content["kind"] = "guest"
|
room_id, latest_event_ids=latest_event_ids,
|
||||||
|
|
||||||
event, context = yield msg_handler.create_event(
|
|
||||||
{
|
|
||||||
"type": EventTypes.Member,
|
|
||||||
"content": content,
|
|
||||||
"room_id": room_id,
|
|
||||||
"sender": requester.user.to_string(),
|
|
||||||
"state_key": target.to_string(),
|
|
||||||
|
|
||||||
# For backwards compatibility:
|
|
||||||
"membership": effective_membership_state,
|
|
||||||
},
|
|
||||||
token_id=requester.access_token_id,
|
|
||||||
txn_id=txn_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
old_state = context.current_state.get((EventTypes.Member, event.state_key))
|
old_state = current_state.get((EventTypes.Member, target.to_string()))
|
||||||
old_membership = old_state.content.get("membership") if old_state else None
|
old_membership = old_state.content.get("membership") if old_state else None
|
||||||
if action == "unban" and old_membership != "ban":
|
if action == "unban" and old_membership != "ban":
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
|
@ -156,13 +219,58 @@ class RoomMemberHandler(BaseHandler):
|
||||||
errcode=Codes.BAD_STATE
|
errcode=Codes.BAD_STATE
|
||||||
)
|
)
|
||||||
|
|
||||||
member_handler = self.hs.get_handlers().room_member_handler
|
is_host_in_room = self.is_host_in_room(current_state)
|
||||||
yield member_handler.send_membership_event(
|
|
||||||
requester,
|
if effective_membership_state == Membership.JOIN:
|
||||||
event,
|
if requester.is_guest and not self._can_guest_join(current_state):
|
||||||
context,
|
# This should be an auth check, but guests are a local concept,
|
||||||
|
# so don't really fit into the general auth process.
|
||||||
|
raise AuthError(403, "Guest access not allowed")
|
||||||
|
|
||||||
|
if not is_host_in_room:
|
||||||
|
inviter = yield self.get_inviter(target.to_string(), room_id)
|
||||||
|
if inviter and not self.hs.is_mine(inviter):
|
||||||
|
remote_room_hosts.append(inviter.domain)
|
||||||
|
|
||||||
|
content = {"membership": Membership.JOIN}
|
||||||
|
if requester.is_guest:
|
||||||
|
content["kind"] = "guest"
|
||||||
|
|
||||||
|
ret = yield self.remote_join(
|
||||||
|
remote_room_hosts, room_id, target, content
|
||||||
|
)
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
elif effective_membership_state == Membership.LEAVE:
|
||||||
|
if not is_host_in_room:
|
||||||
|
# perhaps we've been invited
|
||||||
|
inviter = yield self.get_inviter(target.to_string(), room_id)
|
||||||
|
if not inviter:
|
||||||
|
raise SynapseError(404, "Not a known room")
|
||||||
|
|
||||||
|
if self.hs.is_mine(inviter):
|
||||||
|
# the inviter was on our server, but has now left. Carry on
|
||||||
|
# with the normal rejection codepath.
|
||||||
|
#
|
||||||
|
# This is a bit of a hack, because the room might still be
|
||||||
|
# active on other servers.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# send the rejection to the inviter's HS.
|
||||||
|
remote_room_hosts = remote_room_hosts + [inviter.domain]
|
||||||
|
ret = yield self.reject_remote_invite(
|
||||||
|
target.to_string(), room_id, remote_room_hosts
|
||||||
|
)
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
yield self._local_membership_update(
|
||||||
|
requester=requester,
|
||||||
|
target=target,
|
||||||
|
room_id=room_id,
|
||||||
|
membership=effective_membership_state,
|
||||||
|
txn_id=txn_id,
|
||||||
ratelimit=ratelimit,
|
ratelimit=ratelimit,
|
||||||
remote_room_hosts=remote_room_hosts,
|
prev_event_ids=latest_event_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -211,73 +319,19 @@ class RoomMemberHandler(BaseHandler):
|
||||||
if prev_event is not None:
|
if prev_event is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
action = "send"
|
|
||||||
|
|
||||||
if event.membership == Membership.JOIN:
|
if event.membership == Membership.JOIN:
|
||||||
if requester.is_guest and not self._can_guest_join(context.current_state):
|
if requester.is_guest and not self._can_guest_join(context.current_state):
|
||||||
# This should be an auth check, but guests are a local concept,
|
# This should be an auth check, but guests are a local concept,
|
||||||
# so don't really fit into the general auth process.
|
# so don't really fit into the general auth process.
|
||||||
raise AuthError(403, "Guest access not allowed")
|
raise AuthError(403, "Guest access not allowed")
|
||||||
do_remote_join_dance, remote_room_hosts = self._should_do_dance(
|
|
||||||
context,
|
|
||||||
(self.get_inviter(event.state_key, context.current_state)),
|
|
||||||
remote_room_hosts,
|
|
||||||
)
|
|
||||||
if do_remote_join_dance:
|
|
||||||
action = "remote_join"
|
|
||||||
elif event.membership == Membership.LEAVE:
|
|
||||||
is_host_in_room = self.is_host_in_room(context.current_state)
|
|
||||||
|
|
||||||
if not is_host_in_room:
|
yield self.handle_new_client_event(
|
||||||
# perhaps we've been invited
|
requester,
|
||||||
inviter = self.get_inviter(
|
event,
|
||||||
target_user.to_string(), context.current_state
|
context,
|
||||||
)
|
extra_users=[target_user],
|
||||||
if not inviter:
|
ratelimit=ratelimit,
|
||||||
raise SynapseError(404, "Not a known room")
|
)
|
||||||
|
|
||||||
if self.hs.is_mine(inviter):
|
|
||||||
# the inviter was on our server, but has now left. Carry on
|
|
||||||
# with the normal rejection codepath.
|
|
||||||
#
|
|
||||||
# This is a bit of a hack, because the room might still be
|
|
||||||
# active on other servers.
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# send the rejection to the inviter's HS.
|
|
||||||
remote_room_hosts = remote_room_hosts + [inviter.domain]
|
|
||||||
action = "remote_reject"
|
|
||||||
|
|
||||||
federation_handler = self.hs.get_handlers().federation_handler
|
|
||||||
|
|
||||||
if action == "remote_join":
|
|
||||||
if len(remote_room_hosts) == 0:
|
|
||||||
raise SynapseError(404, "No known servers")
|
|
||||||
|
|
||||||
# We don't do an auth check if we are doing an invite
|
|
||||||
# join dance for now, since we're kinda implicitly checking
|
|
||||||
# that we are allowed to join when we decide whether or not we
|
|
||||||
# need to do the invite/join dance.
|
|
||||||
yield federation_handler.do_invite_join(
|
|
||||||
remote_room_hosts,
|
|
||||||
event.room_id,
|
|
||||||
event.user_id,
|
|
||||||
event.content,
|
|
||||||
)
|
|
||||||
elif action == "remote_reject":
|
|
||||||
yield federation_handler.do_remotely_reject_invite(
|
|
||||||
remote_room_hosts,
|
|
||||||
room_id,
|
|
||||||
event.user_id
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
yield self.handle_new_client_event(
|
|
||||||
requester,
|
|
||||||
event,
|
|
||||||
context,
|
|
||||||
extra_users=[target_user],
|
|
||||||
ratelimit=ratelimit,
|
|
||||||
)
|
|
||||||
|
|
||||||
prev_member_event = context.current_state.get(
|
prev_member_event = context.current_state.get(
|
||||||
(EventTypes.Member, target_user.to_string()),
|
(EventTypes.Member, target_user.to_string()),
|
||||||
|
@ -306,11 +360,11 @@ class RoomMemberHandler(BaseHandler):
|
||||||
and guest_access.content["guest_access"] == "can_join"
|
and guest_access.content["guest_access"] == "can_join"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _should_do_dance(self, context, inviter, room_hosts=None):
|
def _should_do_dance(self, current_state, inviter, room_hosts=None):
|
||||||
# TODO: Shouldn't this be remote_room_host?
|
# TODO: Shouldn't this be remote_room_host?
|
||||||
room_hosts = room_hosts or []
|
room_hosts = room_hosts or []
|
||||||
|
|
||||||
is_host_in_room = self.is_host_in_room(context.current_state)
|
is_host_in_room = self.is_host_in_room(current_state)
|
||||||
if is_host_in_room:
|
if is_host_in_room:
|
||||||
return False, room_hosts
|
return False, room_hosts
|
||||||
|
|
||||||
|
@ -344,11 +398,11 @@ class RoomMemberHandler(BaseHandler):
|
||||||
|
|
||||||
defer.returnValue((RoomID.from_string(room_id), servers))
|
defer.returnValue((RoomID.from_string(room_id), servers))
|
||||||
|
|
||||||
def get_inviter(self, user_id, current_state):
|
@defer.inlineCallbacks
|
||||||
prev_state = current_state.get((EventTypes.Member, user_id))
|
def get_inviter(self, user_id, room_id):
|
||||||
if prev_state and prev_state.membership == Membership.INVITE:
|
invite = yield self.store.get_room_member(user_id=user_id, room_id=room_id)
|
||||||
return UserID.from_string(prev_state.user_id)
|
if invite:
|
||||||
return None
|
defer.returnValue(UserID.from_string(invite.sender))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_joined_rooms_for_user(self, user):
|
def get_joined_rooms_for_user(self, user):
|
||||||
|
|
|
@ -75,7 +75,8 @@ class StateHandler(object):
|
||||||
self._state_cache.start()
|
self._state_cache.start()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_current_state(self, room_id, event_type=None, state_key=""):
|
def get_current_state(self, room_id, event_type=None, state_key="",
|
||||||
|
latest_event_ids=None):
|
||||||
""" Retrieves the current state for the room. This is done by
|
""" Retrieves the current state for the room. This is done by
|
||||||
calling `get_latest_events_in_room` to get the leading edges of the
|
calling `get_latest_events_in_room` to get the leading edges of the
|
||||||
event graph and then resolving any of the state conflicts.
|
event graph and then resolving any of the state conflicts.
|
||||||
|
@ -89,9 +90,10 @@ class StateHandler(object):
|
||||||
Returns:
|
Returns:
|
||||||
map from (type, state_key) to event
|
map from (type, state_key) to event
|
||||||
"""
|
"""
|
||||||
event_ids = yield self.store.get_latest_event_ids_in_room(room_id)
|
if not latest_event_ids:
|
||||||
|
latest_event_ids = yield self.store.get_latest_event_ids_in_room(room_id)
|
||||||
|
|
||||||
res = yield self.resolve_state_groups(room_id, event_ids)
|
res = yield self.resolve_state_groups(room_id, latest_event_ids)
|
||||||
state = res[1]
|
state = res[1]
|
||||||
|
|
||||||
if event_type:
|
if event_type:
|
||||||
|
|
|
@ -163,6 +163,22 @@ class EventFederationStore(SQLBaseStore):
|
||||||
room_id,
|
room_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_max_depth_of_events(self, event_ids):
|
||||||
|
sql = (
|
||||||
|
"SELECT MAX(depth) FROM events WHERE event_id IN (%s)"
|
||||||
|
) % (",".join(["?"] * len(event_ids)),)
|
||||||
|
|
||||||
|
rows = yield self._execute(
|
||||||
|
"get_max_depth_of_events", None,
|
||||||
|
sql, *event_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
defer.returnValue(rows[0][0])
|
||||||
|
else:
|
||||||
|
defer.returnValue(1)
|
||||||
|
|
||||||
def _get_min_depth_interaction(self, txn, room_id):
|
def _get_min_depth_interaction(self, txn, room_id):
|
||||||
min_depth = self._simple_select_one_onecol_txn(
|
min_depth = self._simple_select_one_onecol_txn(
|
||||||
txn,
|
txn,
|
||||||
|
|
|
@ -259,8 +259,8 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||||
# set [invite/join/left] of self, set [invite/join/left] of other,
|
# set [invite/join/left] of self, set [invite/join/left] of other,
|
||||||
# expect all 404s because room doesn't exist on any server
|
# expect all 404s because room doesn't exist on any server
|
||||||
for usr in [self.user_id, self.rmcreator_id]:
|
for usr in [self.user_id, self.rmcreator_id]:
|
||||||
yield self.join(room=room, user=usr, expect_code=404)
|
yield self.join(room=room, user=usr, expect_code=403)
|
||||||
yield self.leave(room=room, user=usr, expect_code=404)
|
yield self.leave(room=room, user=usr, expect_code=403)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_membership_private_room_perms(self):
|
def test_membership_private_room_perms(self):
|
||||||
|
|
Loading…
Reference in New Issue