diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py
index a516a84a66..5613bd2059 100644
--- a/synapse/handlers/_base.py
+++ b/synapse/handlers/_base.py
@@ -53,9 +53,15 @@ class BaseHandler(object):
self.event_builder_factory = hs.get_event_builder_factory()
@defer.inlineCallbacks
- def _filter_events_for_clients(self, user_tuples, events, event_id_to_state):
+ def filter_events_for_clients(self, user_tuples, events, event_id_to_state):
""" Returns dict of user_id -> list of events that user is allowed to
see.
+
+ :param (str, bool) user_tuples: (user id, is_peeking) for each
+ user to be checked. is_peeking should be true if:
+ * the user is not currently a member of the room, and:
+ * the user has not been a member of the room since the given
+ events
"""
forgotten = yield defer.gatherResults([
self.store.who_forgot_in_room(
@@ -72,18 +78,20 @@ class BaseHandler(object):
def allowed(event, user_id, is_peeking):
state = event_id_to_state[event.event_id]
+ # get the room_visibility at the time of the event.
visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None)
if visibility_event:
visibility = visibility_event.content.get("history_visibility", "shared")
else:
visibility = "shared"
+ # if it was world_readable, it's easy: everyone can read it
if visibility == "world_readable":
return True
- if is_peeking:
- return False
-
+ # get the user's membership at the time of the event. (or rather,
+ # just *after* the event. Which means that people can see their
+ # own join events, but not (currently) their own leave events.)
membership_event = state.get((EventTypes.Member, user_id), None)
if membership_event:
if membership_event.event_id in event_id_forgotten:
@@ -93,20 +101,29 @@ class BaseHandler(object):
else:
membership = None
+ # if the user was a member of the room at the time of the event,
+ # they can see it.
if membership == Membership.JOIN:
return True
- if event.type == EventTypes.RoomHistoryVisibility:
- return not is_peeking
+ if visibility == "joined":
+ # we weren't a member at the time of the event, so we can't
+ # see this event.
+ return False
- if visibility == "shared":
- return True
- elif visibility == "joined":
- return membership == Membership.JOIN
elif visibility == "invited":
+ # user can also see the event if they were *invited* at the time
+ # of the event.
return membership == Membership.INVITE
- return True
+ else:
+ # visibility is shared: user can also see the event if they have
+ # become a member since the event
+ #
+ # XXX: if the user has subsequently joined and then left again,
+ # ideally we would share history up to the point they left. But
+ # we don't know when they left.
+ return not is_peeking
defer.returnValue({
user_id: [
@@ -119,7 +136,17 @@ class BaseHandler(object):
@defer.inlineCallbacks
def _filter_events_for_client(self, user_id, events, is_peeking=False):
- # Assumes that user has at some point joined the room if not is_guest.
+ """
+ Check which events a user is allowed to see
+
+ :param str user_id: user id to be checked
+ :param [synapse.events.EventBase] events: list of events to be checked
+ :param bool is_peeking should be True if:
+ * the user is not currently a member of the room, and:
+ * the user has not been a member of the room since the given
+ events
+ :rtype [synapse.events.EventBase]
+ """
types = (
(EventTypes.RoomHistoryVisibility, ""),
(EventTypes.Member, user_id),
@@ -128,7 +155,7 @@ class BaseHandler(object):
frozenset(e.event_id for e in events),
types=types
)
- res = yield self._filter_events_for_clients(
+ res = yield self.filter_events_for_clients(
[(user_id, is_peeking)], events, event_id_to_state
)
defer.returnValue(res.get(user_id, []))
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index ac15f9e5dd..3655b9e5e2 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -14,6 +14,9 @@
# limitations under the License.
"""Contains handlers for federation events."""
+from signedjson.key import decode_verify_key_bytes
+from signedjson.sign import verify_signed_json
+from unpaddedbase64 import decode_base64
from ._base import BaseHandler
@@ -1620,19 +1623,15 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks
@log_function
- def exchange_third_party_invite(self, invite):
- sender = invite["sender"]
- room_id = invite["room_id"]
-
- if "signed" not in invite or "token" not in invite["signed"]:
- logger.info(
- "Discarding received notification of third party invite "
- "without signed: %s" % (invite,)
- )
- return
-
+ def exchange_third_party_invite(
+ self,
+ sender_user_id,
+ target_user_id,
+ room_id,
+ signed,
+ ):
third_party_invite = {
- "signed": invite["signed"],
+ "signed": signed,
}
event_dict = {
@@ -1642,8 +1641,8 @@ class FederationHandler(BaseHandler):
"third_party_invite": third_party_invite,
},
"room_id": room_id,
- "sender": sender,
- "state_key": invite["mxid"],
+ "sender": sender_user_id,
+ "state_key": target_user_id,
}
if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)):
@@ -1656,11 +1655,11 @@ class FederationHandler(BaseHandler):
)
self.auth.check(event, context.current_state)
- yield self._validate_keyserver(event, auth_events=context.current_state)
+ yield self._check_signature(event, auth_events=context.current_state)
member_handler = self.hs.get_handlers().room_member_handler
yield member_handler.send_membership_event(event, context, from_client=False)
else:
- destinations = set([x.split(":", 1)[-1] for x in (sender, room_id)])
+ destinations = set(x.split(":", 1)[-1] for x in (sender_user_id, room_id))
yield self.replication_layer.forward_third_party_invite(
destinations,
room_id,
@@ -1681,7 +1680,7 @@ class FederationHandler(BaseHandler):
)
self.auth.check(event, auth_events=context.current_state)
- yield self._validate_keyserver(event, auth_events=context.current_state)
+ yield self._check_signature(event, auth_events=context.current_state)
returned_invite = yield self.send_invite(origin, event)
# TODO: Make sure the signatures actually are correct.
@@ -1711,17 +1710,69 @@ class FederationHandler(BaseHandler):
defer.returnValue((event, context))
@defer.inlineCallbacks
- def _validate_keyserver(self, event, auth_events):
- token = event.content["third_party_invite"]["signed"]["token"]
+ def _check_signature(self, event, auth_events):
+ """
+ Checks that the signature in the event is consistent with its invite.
+ :param event (Event): The m.room.member event to check
+ :param auth_events (dict<(event type, state_key), event>)
+
+ :raises
+ AuthError if signature didn't match any keys, or key has been
+ revoked,
+ SynapseError if a transient error meant a key couldn't be checked
+ for revocation.
+ """
+ signed = event.content["third_party_invite"]["signed"]
+ token = signed["token"]
invite_event = auth_events.get(
(EventTypes.ThirdPartyInvite, token,)
)
+ if not invite_event:
+ raise AuthError(403, "Could not find invite")
+
+ last_exception = None
+ for public_key_object in self.hs.get_auth().get_public_keys(invite_event):
+ try:
+ for server, signature_block in signed["signatures"].items():
+ for key_name, encoded_signature in signature_block.items():
+ if not key_name.startswith("ed25519:"):
+ continue
+
+ public_key = public_key_object["public_key"]
+ verify_key = decode_verify_key_bytes(
+ key_name,
+ decode_base64(public_key)
+ )
+ verify_signed_json(signed, server, verify_key)
+ if "key_validity_url" in public_key_object:
+ yield self._check_key_revocation(
+ public_key,
+ public_key_object["key_validity_url"]
+ )
+ return
+ except Exception as e:
+ last_exception = e
+ raise last_exception
+
+ @defer.inlineCallbacks
+ def _check_key_revocation(self, public_key, url):
+ """
+ Checks whether public_key has been revoked.
+
+ :param public_key (str): base-64 encoded public key.
+ :param url (str): Key revocation URL.
+
+ :raises
+ AuthError if they key has been revoked.
+ SynapseError if a transient error meant a key couldn't be checked
+ for revocation.
+ """
try:
response = yield self.hs.get_simple_http_client().get_json(
- invite_event.content["key_validity_url"],
- {"public_key": invite_event.content["public_key"]}
+ url,
+ {"public_key": public_key}
)
except Exception:
raise SynapseError(
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index 952e48e319..8134c5185a 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -525,6 +525,7 @@ class PresenceHandler(BaseHandler):
new_fields["last_active_ts"] = now - last_active_ago
new_fields["status_msg"] = push.get("status_msg", None)
+ new_fields["currently_active"] = push.get("currently_active", False)
prev_state = yield self.current_state_for_user(user_id)
updates.append(prev_state.copy_and_replace(**new_fields))
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index b00cac4bd4..eb9700a35b 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -398,6 +398,7 @@ class RoomMemberHandler(BaseHandler):
action,
txn_id=None,
remote_room_hosts=None,
+ third_party_signed=None,
ratelimit=True,
):
effective_membership_state = action
@@ -406,6 +407,15 @@ class RoomMemberHandler(BaseHandler):
elif action == "forget":
effective_membership_state = "leave"
+ if third_party_signed is not None:
+ replication = self.hs.get_replication_layer()
+ yield replication.exchange_third_party_invite(
+ third_party_signed["sender"],
+ target.to_string(),
+ room_id,
+ third_party_signed,
+ )
+
msg_handler = self.hs.get_handlers().message_handler
content = {"membership": effective_membership_state}
@@ -759,7 +769,7 @@ class RoomMemberHandler(BaseHandler):
if room_avatar_event:
room_avatar_url = room_avatar_event.content.get("url", "")
- token, public_key, key_validity_url, display_name = (
+ token, public_keys, fallback_public_key, display_name = (
yield self._ask_id_server_for_third_party_invite(
id_server=id_server,
medium=medium,
@@ -774,14 +784,18 @@ class RoomMemberHandler(BaseHandler):
inviter_avatar_url=inviter_avatar_url
)
)
+
msg_handler = self.hs.get_handlers().message_handler
yield msg_handler.create_and_send_nonmember_event(
{
"type": EventTypes.ThirdPartyInvite,
"content": {
"display_name": display_name,
- "key_validity_url": key_validity_url,
- "public_key": public_key,
+ "public_keys": public_keys,
+
+ # For backwards compatibility:
+ "key_validity_url": fallback_public_key["key_validity_url"],
+ "public_key": fallback_public_key["public_key"],
},
"room_id": room_id,
"sender": user.to_string(),
@@ -806,6 +820,34 @@ class RoomMemberHandler(BaseHandler):
inviter_display_name,
inviter_avatar_url
):
+ """
+ Asks an identity server for a third party invite.
+
+ :param id_server (str): hostname + optional port for the identity server.
+ :param medium (str): The literal string "email".
+ :param address (str): The third party address being invited.
+ :param room_id (str): The ID of the room to which the user is invited.
+ :param inviter_user_id (str): The user ID of the inviter.
+ :param room_alias (str): An alias for the room, for cosmetic
+ notifications.
+ :param room_avatar_url (str): The URL of the room's avatar, for cosmetic
+ notifications.
+ :param room_join_rules (str): The join rules of the email
+ (e.g. "public").
+ :param room_name (str): The m.room.name of the room.
+ :param inviter_display_name (str): The current display name of the
+ inviter.
+ :param inviter_avatar_url (str): The URL of the inviter's avatar.
+
+ :return: A deferred tuple containing:
+ token (str): The token which must be signed to prove authenticity.
+ public_keys ([{"public_key": str, "key_validity_url": str}]):
+ public_key is a base64-encoded ed25519 public key.
+ fallback_public_key: One element from public_keys.
+ display_name (str): A user-friendly name to represent the invited
+ user.
+ """
+
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
id_server_scheme, id_server,
)
@@ -826,12 +868,21 @@ class RoomMemberHandler(BaseHandler):
)
# TODO: Check for success
token = data["token"]
- public_key = data["public_key"]
+ public_keys = data.get("public_keys", [])
+ if "public_key" in data:
+ fallback_public_key = {
+ "public_key": data["public_key"],
+ "key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
+ id_server_scheme, id_server,
+ ),
+ }
+ else:
+ fallback_public_key = public_keys[0]
+
+ if not public_keys:
+ public_keys.append(fallback_public_key)
display_name = data["display_name"]
- key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
- id_server_scheme, id_server,
- )
- defer.returnValue((token, public_key, key_validity_url, display_name))
+ defer.returnValue((token, public_keys, fallback_public_key, display_name))
def forget(self, user, room_id):
return self.store.forget(user.to_string(), room_id)
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 558c7bacb9..fded6e4009 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -121,7 +121,11 @@ class SyncResult(collections.namedtuple("SyncResult", [
events.
"""
return bool(
- self.presence or self.joined or self.invited or self.archived
+ self.presence or
+ self.joined or
+ self.invited or
+ self.archived or
+ self.account_data
)
@@ -645,7 +649,6 @@ class SyncHandler(BaseHandler):
recents = yield self._filter_events_for_client(
sync_config.user.to_string(),
recents,
- is_peeking=sync_config.is_guest,
)
else:
recents = []
@@ -667,7 +670,6 @@ class SyncHandler(BaseHandler):
loaded_recents = yield self._filter_events_for_client(
sync_config.user.to_string(),
loaded_recents,
- is_peeking=sync_config.is_guest,
)
loaded_recents.extend(recents)
recents = loaded_recents
|