From a88e16152f00719df152eaef31dcfd457c019293 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 1 Sep 2015 15:09:23 +0100 Subject: Add flag which disables federation of the room --- synapse/handlers/room.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index c5d1001b50..4f8ad824b5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -150,12 +150,15 @@ class RoomCreationHandler(BaseHandler): for val in raw_initial_state: initial_state[(val["type"], val.get("state_key", ""))] = val["content"] + creation_content = config.get("creation_content", {}) + user = UserID.from_string(user_id) creation_events = self._create_events_for_new_room( user, room_id, preset_config=preset_config, invite_list=invite_list, initial_state=initial_state, + creation_content=creation_content, ) msg_handler = self.hs.get_handlers().message_handler @@ -203,7 +206,7 @@ class RoomCreationHandler(BaseHandler): defer.returnValue(result) def _create_events_for_new_room(self, creator, room_id, preset_config, - invite_list, initial_state): + invite_list, initial_state, creation_content): config = RoomCreationHandler.PRESETS_DICT[preset_config] creator_id = creator.to_string() @@ -225,9 +228,10 @@ class RoomCreationHandler(BaseHandler): return e + creation_content.update({"creator": creator.to_string()}) creation_event = create( etype=EventTypes.Create, - content={"creator": creator.to_string()}, + content=creation_content, ) join_event = create( -- cgit 1.4.1 From e530208e68a2d0c37732855e4ed8f22a9f84d334 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 9 Sep 2015 09:57:49 +0100 Subject: Change default history visibility for private rooms --- synapse/handlers/room.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index c5d1001b50..bb3d428288 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -39,7 +39,7 @@ class RoomCreationHandler(BaseHandler): PRESETS_DICT = { RoomCreationPreset.PRIVATE_CHAT: { "join_rules": JoinRules.INVITE, - "history_visibility": "invited", + "history_visibility": "shared", "original_invitees_have_ops": False, }, RoomCreationPreset.PUBLIC_CHAT: { -- cgit 1.4.1 From bc8b25eb56bf4fcec3546c2ea28741189a519da5 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 9 Sep 2015 15:42:16 +0100 Subject: Allow users that have left the room to view the member list from the point they left --- synapse/handlers/room.py | 36 ------------------------------------ synapse/rest/client/v1/room.py | 18 +++++++++++++----- tests/rest/client/v1/test_rooms.py | 4 ++-- 3 files changed, 15 insertions(+), 43 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index c5d1001b50..0ff816d53e 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -25,7 +25,6 @@ from synapse.api.constants import ( from synapse.api.errors import StoreError, SynapseError from synapse.util import stringutils, unwrapFirstError from synapse.util.async import run_on_reactor -from synapse.events.utils import serialize_event from collections import OrderedDict import logging @@ -342,41 +341,6 @@ class RoomMemberHandler(BaseHandler): if remotedomains is not None: remotedomains.add(member.domain) - @defer.inlineCallbacks - def get_room_members_as_pagination_chunk(self, room_id=None, user_id=None, - limit=0, start_tok=None, - end_tok=None): - """Retrieve a list of room members in the room. - - Args: - room_id (str): The room to get the member list for. - user_id (str): The ID of the user making the request. - limit (int): The max number of members to return. - start_tok (str): Optional. The start token if known. - end_tok (str): Optional. The end token if known. - Returns: - dict: A Pagination streamable dict. - Raises: - SynapseError if something goes wrong. - """ - yield self.auth.check_joined_room(room_id, user_id) - - member_list = yield self.store.get_room_members(room_id=room_id) - time_now = self.clock.time_msec() - event_list = [ - serialize_event(entry, time_now) - for entry in member_list - ] - chunk_data = { - "start": "START", # FIXME (erikj): START is no longer valid - "end": "END", - "chunk": event_list - } - # TODO honor Pagination stream params - # TODO snapshot this list to return on subsequent requests when - # paginating - defer.returnValue(chunk_data) - @defer.inlineCallbacks def change_membership(self, event, context, do_auth=True): """ Change the membership status of a user in a room. diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index c9c27dd5a0..f4558b95a7 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -290,12 +290,18 @@ class RoomMemberListRestServlet(ClientV1RestServlet): def on_GET(self, request, room_id): # TODO support Pagination stream API (limit/tokens) user, _ = yield self.auth.get_user_by_req(request) - handler = self.handlers.room_member_handler - members = yield handler.get_room_members_as_pagination_chunk( + handler = self.handlers.message_handler + events = yield handler.get_state_events( room_id=room_id, - user_id=user.to_string()) + user_id=user.to_string(), + ) + + chunk = [] - for event in members["chunk"]: + for event in events: + if event["type"] != EventTypes.Member: + continue + chunk.append(event) # FIXME: should probably be state_key here, not user_id target_user = UserID.from_string(event["user_id"]) # Presence is an optional cache; don't fail if we can't fetch it @@ -308,7 +314,9 @@ class RoomMemberListRestServlet(ClientV1RestServlet): except: pass - defer.returnValue((200, members)) + defer.returnValue((200, { + "chunk": chunk + })) # TODO: Needs unit testing diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 34ab47d02e..d50cfe4298 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -492,9 +492,9 @@ class RoomsMemberListTestCase(RestTestCase): self.assertEquals(200, code, msg=str(response)) yield self.leave(room=room_id, user=self.user_id) - # can no longer see list, you've left. + # can see old list once left (code, response) = yield self.mock_resource.trigger_get(room_path) - self.assertEquals(403, code, msg=str(response)) + self.assertEquals(200, code, msg=str(response)) class RoomsCreateTestCase(RestTestCase): -- cgit 1.4.1 From 3c166a24c591afdc851de3c6c754c90471b1b0a9 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 9 Sep 2015 16:05:09 +0100 Subject: Remove undocumented and unimplemented 'feedback' parameter from the Client-Server API --- synapse/api/constants.py | 11 ----------- synapse/handlers/message.py | 21 +++------------------ synapse/handlers/room.py | 1 - synapse/rest/client/v1/initial_sync.py | 2 -- synapse/rest/client/v1/room.py | 2 -- synapse/storage/stream.py | 10 ++-------- 6 files changed, 5 insertions(+), 42 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 1423986c1e..3385664394 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -27,16 +27,6 @@ class Membership(object): LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN) -class Feedback(object): - - """Represents the types of feedback a user can send in response to a - message.""" - - DELIVERED = u"delivered" - READ = u"read" - LIST = (DELIVERED, READ) - - class PresenceState(object): """Represents the presence state of a user.""" OFFLINE = u"offline" @@ -73,7 +63,6 @@ class EventTypes(object): PowerLevels = "m.room.power_levels" Aliases = "m.room.aliases" Redaction = "m.room.redaction" - Feedback = "m.room.message.feedback" RoomHistoryVisibility = "m.room.history_visibility" CanonicalAlias = "m.room.canonical_alias" diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 171e9d72ac..72ebac047f 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -71,7 +71,7 @@ class MessageHandler(BaseHandler): @defer.inlineCallbacks def get_messages(self, user_id=None, room_id=None, pagin_config=None, - feedback=False, as_client_event=True): + as_client_event=True): """Get messages in a room. Args: @@ -79,7 +79,6 @@ class MessageHandler(BaseHandler): room_id (str): The room they want messages from. pagin_config (synapse.api.streams.PaginationConfig): The pagination config rules to apply, if any. - feedback (bool): True to get compressed feedback with the messages as_client_event (bool): True to get events in client-server format. Returns: dict: Pagination API results @@ -264,17 +263,6 @@ class MessageHandler(BaseHandler): ) defer.returnValue(data) - @defer.inlineCallbacks - def get_feedback(self, event_id): - # yield self.auth.check_joined_room(room_id, user_id) - - # Pull out the feedback from the db - fb = yield self.store.get_feedback(event_id) - - if fb: - defer.returnValue(fb) - defer.returnValue(None) - @defer.inlineCallbacks def get_state_events(self, user_id, room_id): """Retrieve all state events for a given room. If the user is @@ -303,8 +291,7 @@ class MessageHandler(BaseHandler): ) @defer.inlineCallbacks - def snapshot_all_rooms(self, user_id=None, pagin_config=None, - feedback=False, as_client_event=True): + def snapshot_all_rooms(self, user_id=None, pagin_config=None, as_client_event=True): """Retrieve a snapshot of all rooms the user is invited or has joined. This snapshot may include messages for all rooms where the user is @@ -314,7 +301,6 @@ class MessageHandler(BaseHandler): user_id (str): The ID of the user making the request. pagin_config (synapse.api.streams.PaginationConfig): The pagination config used to determine how many messages *PER ROOM* to return. - feedback (bool): True to get feedback along with these messages. as_client_event (bool): True to get events in client-server format. Returns: A list of dicts with "room_id" and "membership" keys for all rooms @@ -439,8 +425,7 @@ class MessageHandler(BaseHandler): defer.returnValue(ret) @defer.inlineCallbacks - def room_initial_sync(self, user_id, room_id, pagin_config=None, - feedback=False): + def room_initial_sync(self, user_id, room_id, pagin_config=None): """Capture the a snapshot of a room. If user is currently a member of the room this will be what is currently in the room. If the user left the room this will be what was in the room when they left. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 0ff816d53e..243623190f 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -610,7 +610,6 @@ class RoomEventSource(object): to_key=config.to_key, direction=config.direction, limit=config.limit, - with_feedback=True ) defer.returnValue((events, next_key)) diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py index 4ea4da653c..bac68cc29f 100644 --- a/synapse/rest/client/v1/initial_sync.py +++ b/synapse/rest/client/v1/initial_sync.py @@ -26,14 +26,12 @@ class InitialSyncRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_GET(self, request): user, _ = yield self.auth.get_user_by_req(request) - with_feedback = "feedback" in request.args as_client_event = "raw" not in request.args pagination_config = PaginationConfig.from_request(request) handler = self.handlers.message_handler content = yield handler.snapshot_all_rooms( user_id=user.to_string(), pagin_config=pagination_config, - feedback=with_feedback, as_client_event=as_client_event ) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index f4558b95a7..23871f161e 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -329,14 +329,12 @@ class RoomMessageListRestServlet(ClientV1RestServlet): pagination_config = PaginationConfig.from_request( request, default_limit=10, ) - with_feedback = "feedback" in request.args as_client_event = "raw" not in request.args handler = self.handlers.message_handler msgs = yield handler.get_messages( room_id=room_id, user_id=user.to_string(), pagin_config=pagination_config, - feedback=with_feedback, as_client_event=as_client_event ) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index 0abfa86cd2..5763c462af 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -159,9 +159,7 @@ class StreamStore(SQLBaseStore): @log_function def get_room_events_stream(self, user_id, from_key, to_key, room_id, - limit=0, with_feedback=False): - # TODO (erikj): Handle compressed feedback - + limit=0): current_room_membership_sql = ( "SELECT m.room_id FROM room_memberships as m " " INNER JOIN current_state_events as c" @@ -227,10 +225,7 @@ class StreamStore(SQLBaseStore): @defer.inlineCallbacks def paginate_room_events(self, room_id, from_key, to_key=None, - direction='b', limit=-1, - with_feedback=False): - # TODO (erikj): Handle compressed feedback - + direction='b', limit=-1): # Tokens really represent positions between elements, but we use # the convention of pointing to the event before the gap. Hence # we have a bit of asymmetry when it comes to equalities. @@ -302,7 +297,6 @@ class StreamStore(SQLBaseStore): @cachedInlineCallbacks(num_args=4) def get_recent_events_for_room(self, room_id, limit, end_token, from_token=None): - # TODO (erikj): Handle compressed feedback end_token = RoomStreamToken.parse_stream_token(end_token) -- cgit 1.4.1 From 257fa1c53e20b4394ff1493f6112a011c2727e7b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 23 Sep 2015 10:07:31 +0100 Subject: Set m.room.canonical_alias on room creation. --- synapse/handlers/room.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index bb5eef6bbd..e194f39e70 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -155,6 +155,7 @@ class RoomCreationHandler(BaseHandler): preset_config=preset_config, invite_list=invite_list, initial_state=initial_state, + room_alias=room_alias, ) msg_handler = self.hs.get_handlers().message_handler @@ -202,7 +203,7 @@ class RoomCreationHandler(BaseHandler): defer.returnValue(result) def _create_events_for_new_room(self, creator, room_id, preset_config, - invite_list, initial_state): + invite_list, initial_state, room_alias): config = RoomCreationHandler.PRESETS_DICT[preset_config] creator_id = creator.to_string() @@ -271,6 +272,15 @@ class RoomCreationHandler(BaseHandler): returned_events.append(power_levels_event) + if room_alias: + if (EventTypes.CanonicalAlias, '') not in initial_state: + room_alias_event = create( + etype=EventTypes.CanonicalAlias, + content={"alias": room_alias.to_string()}, + ) + + returned_events.append(room_alias_event) + if (EventTypes.JoinRules, '') not in initial_state: join_rules_event = create( etype=EventTypes.JoinRules, -- cgit 1.4.1 From 57338a97683de16b325a2e62b4dee4b4a6b77aa2 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 23 Sep 2015 10:26:45 +0100 Subject: synapse/handlers/room.py:_should_invite_join was unused --- synapse/handlers/room.py | 26 -------------------------- 1 file changed, 26 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 243623190f..d60f86bbd0 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -492,32 +492,6 @@ class RoomMemberHandler(BaseHandler): "user_joined_room", user=user, room_id=room_id ) - @defer.inlineCallbacks - def _should_invite_join(self, room_id, prev_state, do_auth): - logger.debug("_should_invite_join: room_id: %s", room_id) - - # XXX: 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. - - # Only do an invite join dance if a) we were invited, - # b) the person inviting was from a differnt HS and c) we are - # not currently in the room - room_host = None - if prev_state and prev_state.membership == Membership.INVITE: - room = yield self.store.get_room(room_id) - inviter = UserID.from_string( - prev_state.sender - ) - - is_remote_invite_join = not self.hs.is_mine(inviter) and not room - room_host = inviter.domain - else: - is_remote_invite_join = False - - defer.returnValue((is_remote_invite_join, room_host)) - @defer.inlineCallbacks def get_joined_rooms_for_user(self, user): """Returns a list of roomids that the user has any of the given -- cgit 1.4.1 From ecd0c0dfc50ceed16aa47cf066bc412211af2335 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Sep 2015 16:46:24 +0100 Subject: Remove double indentation --- synapse/handlers/room.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index e194f39e70..2b15136bda 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -272,14 +272,13 @@ class RoomCreationHandler(BaseHandler): returned_events.append(power_levels_event) - if room_alias: - if (EventTypes.CanonicalAlias, '') not in initial_state: - room_alias_event = create( - etype=EventTypes.CanonicalAlias, - content={"alias": room_alias.to_string()}, - ) + if room_alias and (EventTypes.CanonicalAlias, '') not in initial_state: + room_alias_event = create( + etype=EventTypes.CanonicalAlias, + content={"alias": room_alias.to_string()}, + ) - returned_events.append(room_alias_event) + returned_events.append(room_alias_event) if (EventTypes.JoinRules, '') not in initial_state: join_rules_event = create( -- cgit 1.4.1 From 5b3e9713dd098df95b321f216105b2298deaeb92 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 1 Oct 2015 17:49:52 +0100 Subject: Implement third party identifier invites --- synapse/api/auth.py | 33 +++++++- synapse/api/constants.py | 1 + synapse/federation/federation_client.py | 9 ++- synapse/federation/federation_server.py | 19 ++++- synapse/federation/transport/client.py | 5 +- synapse/federation/transport/server.py | 2 +- synapse/handlers/_base.py | 11 +++ synapse/handlers/federation.py | 16 +++- synapse/handlers/room.py | 4 + synapse/rest/client/v1/room.py | 132 ++++++++++++++++++++++++++++++-- synapse/util/thirdpartyinvites.py | 62 +++++++++++++++ 11 files changed, 275 insertions(+), 19 deletions(-) create mode 100644 synapse/util/thirdpartyinvites.py (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 847ff60671..37f7f1bf79 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -14,15 +14,19 @@ # limitations under the License. """This module contains classes for authenticating the user.""" +from nacl.exceptions import BadSignatureError from twisted.internet import defer from synapse.api.constants import EventTypes, Membership, JoinRules from synapse.api.errors import AuthError, Codes, SynapseError from synapse.util.logutils import log_function +from synapse.util.thirdpartyinvites import ThirdPartyInvites from synapse.types import UserID, EventID +from unpaddedbase64 import decode_base64 import logging +import nacl.signing import pymacaroons logger = logging.getLogger(__name__) @@ -31,6 +35,7 @@ logger = logging.getLogger(__name__) AuthEventTypes = ( EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels, EventTypes.JoinRules, EventTypes.RoomHistoryVisibility, + EventTypes.ThirdPartyInvite, ) @@ -318,7 +323,8 @@ class Auth(object): pass elif join_rule == JoinRules.INVITE: if not caller_in_room and not caller_invited: - raise AuthError(403, "You are not invited to this room.") + if not self._verify_third_party_invite(event, auth_events): + raise AuthError(403, "You are not invited to this room.") else: # TODO (erikj): may_join list # TODO (erikj): private rooms @@ -344,6 +350,31 @@ class Auth(object): return True + def _verify_third_party_invite(self, event, auth_events): + for key in ThirdPartyInvites.JOIN_KEYS: + if key not in event.content: + return False + token = event.content["token"] + invite_event = auth_events.get( + (EventTypes.ThirdPartyInvite, token,) + ) + if not invite_event: + return False + try: + public_key = event.content["public_key"] + key_validity_url = event.content["key_validity_url"] + if invite_event.content["public_key"] != public_key: + return False + if invite_event.content["key_validity_url"] != key_validity_url: + return False + verify_key = nacl.signing.VerifyKey(decode_base64(public_key)) + encoded_signature = event.content["signature"] + signature = decode_base64(encoded_signature) + verify_key.verify(token, signature) + return True + except (KeyError, BadSignatureError,): + return False + def _get_power_level_event(self, auth_events): key = (EventTypes.PowerLevels, "", ) return auth_events.get(key) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 3385664394..bfc230d126 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -63,6 +63,7 @@ class EventTypes(object): PowerLevels = "m.room.power_levels" Aliases = "m.room.aliases" Redaction = "m.room.redaction" + ThirdPartyInvite = "m.room.third_party_invite" RoomHistoryVisibility = "m.room.history_visibility" CanonicalAlias = "m.room.canonical_alias" diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index f5e346cdbc..bf22913d4f 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -25,6 +25,7 @@ from synapse.api.errors import ( from synapse.util import unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.logutils import log_function +from synapse.util.thirdpartyinvites import ThirdPartyInvites from synapse.events import FrozenEvent import synapse.metrics @@ -356,18 +357,22 @@ class FederationClient(FederationBase): defer.returnValue(signed_auth) @defer.inlineCallbacks - def make_join(self, destinations, room_id, user_id): + def make_join(self, destinations, room_id, user_id, content): for destination in destinations: if destination == self.server_name: continue + args = {} + if ThirdPartyInvites.has_join_keys(content): + ThirdPartyInvites.copy_join_keys(content, args) try: ret = yield self.transport_layer.make_join( - destination, room_id, user_id + destination, room_id, user_id, args ) pdu_dict = ret["event"] + logger.debug("Got response to make_join: %s", pdu_dict) defer.returnValue( diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 725c6f3fa5..d71ab44271 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -23,10 +23,12 @@ from synapse.util.logutils import log_function from synapse.events import FrozenEvent import synapse.metrics -from synapse.api.errors import FederationError, SynapseError +from synapse.api.errors import FederationError, SynapseError, Codes from synapse.crypto.event_signing import compute_event_signature +from synapse.util.thirdpartyinvites import ThirdPartyInvites + import simplejson as json import logging @@ -228,8 +230,19 @@ class FederationServer(FederationBase): ) @defer.inlineCallbacks - def on_make_join_request(self, room_id, user_id): - pdu = yield self.handler.on_make_join_request(room_id, user_id) + def on_make_join_request(self, room_id, user_id, query): + threepid_details = {} + if ThirdPartyInvites.has_join_keys(query): + for k in ThirdPartyInvites.JOIN_KEYS: + if not isinstance(query[k], list) or len(query[k]) != 1: + raise FederationError( + "FATAL", + Codes.MISSING_PARAM, + "key %s value %s" % (k, query[k],), + None + ) + threepid_details[k] = query[k][0] + pdu = yield self.handler.on_make_join_request(room_id, user_id, threepid_details) time_now = self._clock.time_msec() defer.returnValue({"event": pdu.get_pdu_json(time_now)}) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index ced703364b..ae4195e83a 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -160,13 +160,14 @@ class TransportLayerClient(object): @defer.inlineCallbacks @log_function - def make_join(self, destination, room_id, user_id, retry_on_dns_fail=True): + def make_join(self, destination, room_id, user_id, args={}): path = PREFIX + "/make_join/%s/%s" % (room_id, user_id) content = yield self.client.get_json( destination=destination, path=path, - retry_on_dns_fail=retry_on_dns_fail, + args=args, + retry_on_dns_fail=True, ) defer.returnValue(content) diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 36f250e1a3..6e394f039e 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -292,7 +292,7 @@ class FederationMakeJoinServlet(BaseFederationServlet): @defer.inlineCallbacks def on_GET(self, origin, content, query, context, user_id): - content = yield self.handler.on_make_join_request(context, user_id) + content = yield self.handler.on_make_join_request(context, user_id, query) defer.returnValue((200, content)) diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 60ac6617ae..52434920e3 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -21,6 +21,7 @@ from synapse.api.constants import Membership, EventTypes from synapse.types import UserID, RoomAlias from synapse.util.logcontext import PreserveLoggingContext +from synapse.util.thirdpartyinvites import ThirdPartyInvites import logging @@ -123,6 +124,16 @@ class BaseHandler(object): ) ) + if ( + event.type == EventTypes.Member and + event.content["membership"] == Membership.JOIN and + ThirdPartyInvites.has_join_keys(event.content) + ): + yield ThirdPartyInvites.check_key_valid( + self.hs.get_simple_http_client(), + event + ) + (event_stream_id, max_stream_id) = yield self.store.persist_event( event, context=context ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f4dce712f9..d3d172b7b4 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -39,7 +39,7 @@ from twisted.internet import defer import itertools import logging - +from synapse.util.thirdpartyinvites import ThirdPartyInvites logger = logging.getLogger(__name__) @@ -572,7 +572,8 @@ class FederationHandler(BaseHandler): origin, pdu = yield self.replication_layer.make_join( target_hosts, room_id, - joinee + joinee, + content ) logger.debug("Got response to make_join: %s", pdu) @@ -712,14 +713,18 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks @log_function - def on_make_join_request(self, room_id, user_id): + def on_make_join_request(self, room_id, user_id, query): """ We've received a /make_join/ request, so we create a partial join event for the room and return that. We don *not* persist or process it until the other server has signed it and sent it back. """ + event_content = {"membership": Membership.JOIN} + if ThirdPartyInvites.has_join_keys(query): + ThirdPartyInvites.copy_join_keys(query, event_content) + builder = self.event_builder_factory.new({ "type": EventTypes.Member, - "content": {"membership": Membership.JOIN}, + "content": event_content, "room_id": room_id, "sender": user_id, "state_key": user_id, @@ -731,6 +736,9 @@ class FederationHandler(BaseHandler): self.auth.check(event, auth_events=context.current_state) + if ThirdPartyInvites.has_join_keys(event.content): + ThirdPartyInvites.check_key_valid(self.hs.get_simple_http_client(), event) + defer.returnValue(event) @defer.inlineCallbacks diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 773f0a2e92..1c79bc194a 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -462,6 +462,10 @@ class RoomMemberHandler(BaseHandler): if prev_state and prev_state.membership == Membership.INVITE: inviter = UserID.from_string(prev_state.user_id) + should_do_dance = not self.hs.is_mine(inviter) + room_hosts = [inviter.domain] + elif "sender" in event.content: + inviter = UserID.from_string(event.content["sender"]) should_do_dance = not self.hs.is_mine(inviter) room_hosts = [inviter.domain] else: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 23871f161e..ba37061290 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -17,7 +17,7 @@ from twisted.internet import defer from base import ClientV1RestServlet, client_path_pattern -from synapse.api.errors import SynapseError, Codes +from synapse.api.errors import SynapseError, Codes, AuthError from synapse.streams.config import PaginationConfig from synapse.api.constants import EventTypes, Membership from synapse.types import UserID, RoomID, RoomAlias @@ -26,7 +26,7 @@ from synapse.events.utils import serialize_event import simplejson as json import logging import urllib - +from synapse.util.thirdpartyinvites import ThirdPartyInvites logger = logging.getLogger(__name__) @@ -415,9 +415,35 @@ class RoomMembershipRestServlet(ClientV1RestServlet): # target user is you unless it is an invite state_key = user.to_string() if membership_action in ["invite", "ban", "kick"]: - if "user_id" not in content: - raise SynapseError(400, "Missing user_id key.") - state_key = content["user_id"] + try: + state_key = content["user_id"] + except KeyError: + if ( + membership_action != "invite" or + not ThirdPartyInvites.has_invite_keys(content) + ): + raise SynapseError(400, "Missing user_id key.") + + + id_server = content["id_server"] + medium = content["medium"] + address = content["address"] + display_name = content["display_name"] + state_key = yield self._lookup_3pid_user(id_server, medium, address) + if not state_key: + yield self._make_and_store_3pid_invite( + id_server, + display_name, + medium, + address, + room_id, + user, + token_id, + txn_id=txn_id + ) + defer.returnValue((200, {})) + return + # make sure it looks like a user ID; it'll throw if it's invalid. UserID.from_string(state_key) @@ -425,10 +451,18 @@ class RoomMembershipRestServlet(ClientV1RestServlet): membership_action = "leave" msg_handler = self.handlers.message_handler + + event_content = { + "membership": unicode(membership_action), + } + + if membership_action == "join" and ThirdPartyInvites.has_join_keys(content): + ThirdPartyInvites.copy_join_keys(content, event_content) + yield msg_handler.create_and_send_event( { "type": EventTypes.Member, - "content": {"membership": unicode(membership_action)}, + "content": event_content, "room_id": room_id, "sender": user.to_string(), "state_key": state_key, @@ -439,6 +473,92 @@ class RoomMembershipRestServlet(ClientV1RestServlet): defer.returnValue((200, {})) + @defer.inlineCallbacks + def _lookup_3pid_user(self, id_server, medium, address): + """Looks up a 3pid in the passed identity server. + + Args: + id_server (str): The server name (including port, if required) + of the identity server to use. + medium (str): The type of the third party identifier (e.g. "email"). + address (str): The third party identifier (e.g. "foo@example.com"). + + Returns: + (str) the matrix ID of the 3pid, or None if it is not recognized. + """ + try: + data = yield self.hs.get_simple_http_client().get_json( + "https://%s/_matrix/identity/api/v1/lookup" % (id_server,), + { + "medium": medium, + "address": address, + } + ) + + if "mxid" in data: + # TODO: Validate the response signature and such + defer.returnValue(data["mxid"]) + except IOError: + # TODO: Log something maybe? + defer.returnValue(None) + + @defer.inlineCallbacks + def _make_and_store_3pid_invite( + self, + id_server, + display_name, + medium, + address, + room_id, + user, + token_id, + txn_id + ): + token, public_key, key_validity_url = ( + yield self._ask_id_server_for_third_party_invite( + id_server, + medium, + address, + room_id, + user.to_string() + ) + ) + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event( + { + "type": EventTypes.ThirdPartyInvite, + "content": { + "display_name": display_name, + "key_validity_url": key_validity_url, + "public_key": public_key, + }, + "room_id": room_id, + "sender": user.to_string(), + "state_key": token, + }, + token_id=token_id, + txn_id=txn_id, + ) + + @defer.inlineCallbacks + def _ask_id_server_for_third_party_invite( + self, id_server, medium, address, room_id, sender): + is_url = "https://%s/_matrix/identity/api/v1/nonce-it-up" % (id_server,) + data = yield self.hs.get_simple_http_client().post_urlencoded_get_json( + is_url, + { + "medium": medium, + "address": address, + "room_id": room_id, + "sender": sender, + } + ) + # TODO: Check for success + token = data["token"] + public_key = data["public_key"] + key_validity_url = "https://%s/_matrix/identity/api/v1/pubkey/isvalid" % (id_server,) + defer.returnValue((token, public_key, key_validity_url)) + @defer.inlineCallbacks def on_PUT(self, request, room_id, membership_action, txn_id): try: diff --git a/synapse/util/thirdpartyinvites.py b/synapse/util/thirdpartyinvites.py new file mode 100644 index 0000000000..c30279de67 --- /dev/null +++ b/synapse/util/thirdpartyinvites.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 OpenMarket 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. + +from twisted.internet import defer +from synapse.api.errors import AuthError + + +class ThirdPartyInvites(object): + INVITE_KEYS = {"id_server", "medium", "address", "display_name"} + + JOIN_KEYS = { + "token", + "public_key", + "key_validity_url", + "signature", + "sender", + } + + @classmethod + def has_invite_keys(cls, content): + for key in cls.INVITE_KEYS: + if key not in content: + return False + return True + + @classmethod + def has_join_keys(cls, content): + for key in cls.JOIN_KEYS: + if key not in content: + return False + return True + + @classmethod + def copy_join_keys(cls, src, dst): + for key in cls.JOIN_KEYS: + if key in src: + dst[key] = src[key] + + @classmethod + @defer.inlineCallbacks + def check_key_valid(cls, http_client, event): + try: + response = yield http_client.get_json( + event.content["key_validity_url"], + {"public_key": event.content["public_key"]} + ) + if not response["valid"]: + raise AuthError(403, "Third party certificate was invalid") + except IOError: + raise AuthError(403, "Third party certificate could not be checked") -- cgit 1.4.1 From 40017a9a114aa917d7cb3231da08465f7500ab41 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 2 Oct 2015 11:22:56 +0100 Subject: Add 'trusted_private_chat' to room creation presets --- synapse/api/constants.py | 1 + synapse/handlers/room.py | 5 +++++ 2 files changed, 6 insertions(+) (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 3385664394..008ee64727 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -83,3 +83,4 @@ class RejectedReason(object): class RoomCreationPreset(object): PRIVATE_CHAT = "private_chat" PUBLIC_CHAT = "public_chat" + TRUSTED_PRIVATE_CHAT = "trusted_private_chat" diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index ac636255c2..3364a5de14 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -41,6 +41,11 @@ class RoomCreationHandler(BaseHandler): "history_visibility": "shared", "original_invitees_have_ops": False, }, + RoomCreationPreset.TRUSTED_PRIVATE_CHAT: { + "join_rules": JoinRules.INVITE, + "history_visibility": "shared", + "original_invitees_have_ops": True, + }, RoomCreationPreset.PUBLIC_CHAT: { "join_rules": JoinRules.PUBLIC, "history_visibility": "shared", -- cgit 1.4.1 From 17dffef5ec74d789f68096c95d29cdcad57ce5c7 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Tue, 13 Oct 2015 15:48:12 +0100 Subject: Move event contents into third_party_layout field --- synapse/api/auth.py | 21 ++++++++++++--------- synapse/federation/federation_client.py | 4 ++-- synapse/handlers/_base.py | 2 +- synapse/handlers/federation.py | 5 +++-- synapse/handlers/room.py | 11 +++++++---- synapse/rest/client/v1/room.py | 3 ++- synapse/util/thirdpartyinvites.py | 10 ++++++++-- 7 files changed, 35 insertions(+), 21 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index adb9a776e0..ca280707c5 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -374,24 +374,24 @@ class Auth(object): return True def _verify_third_party_invite(self, event, auth_events): - for key in ThirdPartyInvites.JOIN_KEYS: - if key not in event.content: - return False - token = event.content["token"] + if not ThirdPartyInvites.join_has_third_party_invite(event.content): + return False + join_third_party_invite = event.content["third_party_invite"] + token = join_third_party_invite["token"] invite_event = auth_events.get( (EventTypes.ThirdPartyInvite, token,) ) if not invite_event: return False try: - public_key = event.content["public_key"] - key_validity_url = event.content["key_validity_url"] + public_key = join_third_party_invite["public_key"] + key_validity_url = join_third_party_invite["key_validity_url"] if invite_event.content["public_key"] != public_key: return False if invite_event.content["key_validity_url"] != key_validity_url: return False verify_key = nacl.signing.VerifyKey(decode_base64(public_key)) - encoded_signature = event.content["signature"] + encoded_signature = join_third_party_invite["signature"] signature = decode_base64(encoded_signature) verify_key.verify(token, signature) return True @@ -677,8 +677,11 @@ class Auth(object): if e_type == Membership.JOIN: if member_event and not is_public: auth_ids.append(member_event.event_id) - if ThirdPartyInvites.has_join_keys(event.content): - key = (EventTypes.ThirdPartyInvite, event.content["token"]) + if ThirdPartyInvites.join_has_third_party_invite(event.content): + key = ( + EventTypes.ThirdPartyInvite, + event.content["third_party_invite"]["token"] + ) invite = current_state.get(key) if invite: auth_ids.append(invite.event_id) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 06b0c7adcf..6be83d82e7 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -363,8 +363,8 @@ class FederationClient(FederationBase): continue args = {} - if ThirdPartyInvites.has_join_keys(content): - ThirdPartyInvites.copy_join_keys(content, args) + if ThirdPartyInvites.join_has_third_party_invite(content): + ThirdPartyInvites.copy_join_keys(content["third_party_invite"], args) try: ret = yield self.transport_layer.make_join( destination, room_id, user_id, args diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index faf99f5bd3..4165c56bed 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -127,7 +127,7 @@ class BaseHandler(object): if ( event.type == EventTypes.Member and event.content["membership"] == Membership.JOIN and - ThirdPartyInvites.has_join_keys(event.content) + ThirdPartyInvites.join_has_third_party_invite(event.content) ): yield ThirdPartyInvites.check_key_valid( self.hs.get_simple_http_client(), diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8197d8b2d0..8606c0d285 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -705,7 +705,8 @@ class FederationHandler(BaseHandler): """ event_content = {"membership": Membership.JOIN} if ThirdPartyInvites.has_join_keys(query): - ThirdPartyInvites.copy_join_keys(query, event_content) + event_content["third_party_invite"] = {} + ThirdPartyInvites.copy_join_keys(query, event_content["third_party_invite"]) builder = self.event_builder_factory.new({ "type": EventTypes.Member, @@ -721,7 +722,7 @@ class FederationHandler(BaseHandler): self.auth.check(event, auth_events=context.current_state) - if ThirdPartyInvites.has_join_keys(event.content): + if ThirdPartyInvites.join_has_third_party_invite(event.content): ThirdPartyInvites.check_key_valid(self.hs.get_simple_http_client(), event) defer.returnValue(event) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index b856b424a7..e07472b4b9 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -483,10 +483,13 @@ class RoomMemberHandler(BaseHandler): should_do_dance = not self.hs.is_mine(inviter) room_hosts = [inviter.domain] - elif "sender" in event.content: - inviter = UserID.from_string(event.content["sender"]) - should_do_dance = not self.hs.is_mine(inviter) - room_hosts = [inviter.domain] + elif "third_party_invite" in event.content: + if "sender" in event.content["third_party_invite"]: + inviter = UserID.from_string( + event.content["third_party_invite"]["sender"] + ) + should_do_dance = not self.hs.is_mine(inviter) + room_hosts = [inviter.domain] else: # return the same error as join_room_alias does raise SynapseError(404, "No known servers") diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index ff84affea3..1cb6ba4f1f 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -456,7 +456,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet): } if membership_action == "join" and ThirdPartyInvites.has_join_keys(content): - ThirdPartyInvites.copy_join_keys(content, event_content) + event_content["third_party_invite"] = {} + ThirdPartyInvites.copy_join_keys(content, event_content["third_party_invite"]) yield msg_handler.create_and_send_event( { diff --git a/synapse/util/thirdpartyinvites.py b/synapse/util/thirdpartyinvites.py index c30279de67..ad0f4e88e9 100644 --- a/synapse/util/thirdpartyinvites.py +++ b/synapse/util/thirdpartyinvites.py @@ -42,6 +42,12 @@ class ThirdPartyInvites(object): return False return True + @classmethod + def join_has_third_party_invite(cls, content): + if "third_party_invite" not in content: + return False + return cls.has_join_keys(content["third_party_invite"]) + @classmethod def copy_join_keys(cls, src, dst): for key in cls.JOIN_KEYS: @@ -53,8 +59,8 @@ class ThirdPartyInvites(object): def check_key_valid(cls, http_client, event): try: response = yield http_client.get_json( - event.content["key_validity_url"], - {"public_key": event.content["public_key"]} + event.content["third_party_invite"]["key_validity_url"], + {"public_key": event.content["third_party_invite"]["public_key"]} ) if not response["valid"]: raise AuthError(403, "Third party certificate was invalid") -- cgit 1.4.1 From 14edea1afff6e2b269f073d5561926348f9dee87 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Tue, 13 Oct 2015 17:47:58 +0100 Subject: Move logic into handler --- synapse/handlers/room.py | 132 ++++++++++++++++++++++++++++++++++++++++ synapse/rest/client/v1/room.py | 133 ++++++----------------------------------- 2 files changed, 150 insertions(+), 115 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index e07472b4b9..bbb457e823 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -547,6 +547,138 @@ class RoomMemberHandler(BaseHandler): suppress_auth=(not do_auth), ) + @defer.inlineCallbacks + def do_3pid_invite( + self, + room_id, + inviter, + medium, + address, + id_server, + display_name, + token_id, + txn_id + ): + invitee = yield self._lookup_3pid( + id_server, medium, address + ) + + if invitee: + # make sure it looks like a user ID; it'll throw if it's invalid. + UserID.from_string(invitee) + yield self.handlers.message_handler.create_and_send_event( + { + "type": EventTypes.Member, + "content": { + "membership": unicode("invite") + }, + "room_id": room_id, + "sender": inviter.to_string(), + "state_key": invitee, + }, + token_id=token_id, + txn_id=txn_id, + ) + else: + yield self._make_and_store_3pid_invite( + id_server, + display_name, + medium, + address, + room_id, + inviter, + token_id, + txn_id=txn_id + ) + + @defer.inlineCallbacks + def _lookup_3pid(self, id_server, medium, address): + """Looks up a 3pid in the passed identity server. + + Args: + id_server (str): The server name (including port, if required) + of the identity server to use. + medium (str): The type of the third party identifier (e.g. "email"). + address (str): The third party identifier (e.g. "foo@example.com"). + + Returns: + (str) the matrix ID of the 3pid, or None if it is not recognized. + """ + try: + data = yield self.hs.get_simple_http_client().get_json( + "https://%s/_matrix/identity/api/v1/lookup" % (id_server,), + { + "medium": medium, + "address": address, + } + ) + + if "mxid" in data: + # TODO: Validate the response signature and such + defer.returnValue(data["mxid"]) + except IOError as e: + logger.warn("Error from identity server lookup: %s" % (e,)) + defer.returnValue(None) + + @defer.inlineCallbacks + def _make_and_store_3pid_invite( + self, + id_server, + display_name, + medium, + address, + room_id, + user, + token_id, + txn_id + ): + token, public_key, key_validity_url = ( + yield self._ask_id_server_for_third_party_invite( + id_server, + medium, + address, + room_id, + user.to_string() + ) + ) + msg_handler = self.handlers.message_handler + yield msg_handler.create_and_send_event( + { + "type": EventTypes.ThirdPartyInvite, + "content": { + "display_name": display_name, + "key_validity_url": key_validity_url, + "public_key": public_key, + }, + "room_id": room_id, + "sender": user.to_string(), + "state_key": token, + }, + token_id=token_id, + txn_id=txn_id, + ) + + @defer.inlineCallbacks + def _ask_id_server_for_third_party_invite( + self, id_server, medium, address, room_id, sender): + is_url = "https://%s/_matrix/identity/api/v1/nonce-it-up" % (id_server,) + data = yield self.hs.get_simple_http_client().post_urlencoded_get_json( + is_url, + { + "medium": medium, + "address": address, + "room_id": room_id, + "sender": sender, + } + ) + # TODO: Check for success + token = data["token"] + public_key = data["public_key"] + key_validity_url = "https://%s/_matrix/identity/api/v1/pubkey/isvalid" % ( + id_server, + ) + defer.returnValue((token, public_key, key_validity_url)) + class RoomListHandler(BaseHandler): diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 2cb40df80b..1aca203744 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -414,34 +414,25 @@ class RoomMembershipRestServlet(ClientV1RestServlet): # target user is you unless it is an invite state_key = user.to_string() - if membership_action in ["invite", "ban", "kick"]: - try: + + if membership_action == "invite" and ThirdPartyInvites.has_invite_keys(content): + yield self.handlers.room_member_handler.do_3pid_invite( + room_id, + user, + content["medium"], + content["address"], + content["id_server"], + content["display_name"], + token_id, + txn_id + ) + defer.returnValue((200, {})) + return + elif membership_action in ["invite", "ban", "kick"]: + if "user_id" in content: state_key = content["user_id"] - except KeyError: - if ( - membership_action != "invite" or - not ThirdPartyInvites.has_invite_keys(content) - ): - raise SynapseError(400, "Missing user_id key.") - - id_server = content["id_server"] - medium = content["medium"] - address = content["address"] - display_name = content["display_name"] - state_key = yield self._lookup_3pid_user(id_server, medium, address) - if not state_key: - yield self._make_and_store_3pid_invite( - id_server, - display_name, - medium, - address, - room_id, - user, - token_id, - txn_id=txn_id - ) - defer.returnValue((200, {})) - return + else: + raise SynapseError(400, "Missing user_id key.") # make sure it looks like a user ID; it'll throw if it's invalid. UserID.from_string(state_key) @@ -473,94 +464,6 @@ class RoomMembershipRestServlet(ClientV1RestServlet): defer.returnValue((200, {})) - @defer.inlineCallbacks - def _lookup_3pid_user(self, id_server, medium, address): - """Looks up a 3pid in the passed identity server. - - Args: - id_server (str): The server name (including port, if required) - of the identity server to use. - medium (str): The type of the third party identifier (e.g. "email"). - address (str): The third party identifier (e.g. "foo@example.com"). - - Returns: - (str) the matrix ID of the 3pid, or None if it is not recognized. - """ - try: - data = yield self.hs.get_simple_http_client().get_json( - "https://%s/_matrix/identity/api/v1/lookup" % (id_server,), - { - "medium": medium, - "address": address, - } - ) - - if "mxid" in data: - # TODO: Validate the response signature and such - defer.returnValue(data["mxid"]) - except IOError as e: - logger.warn("Error from identity server lookup: %s" % (e,)) - defer.returnValue(None) - - @defer.inlineCallbacks - def _make_and_store_3pid_invite( - self, - id_server, - display_name, - medium, - address, - room_id, - user, - token_id, - txn_id - ): - token, public_key, key_validity_url = ( - yield self._ask_id_server_for_third_party_invite( - id_server, - medium, - address, - room_id, - user.to_string() - ) - ) - msg_handler = self.handlers.message_handler - yield msg_handler.create_and_send_event( - { - "type": EventTypes.ThirdPartyInvite, - "content": { - "display_name": display_name, - "key_validity_url": key_validity_url, - "public_key": public_key, - }, - "room_id": room_id, - "sender": user.to_string(), - "state_key": token, - }, - token_id=token_id, - txn_id=txn_id, - ) - - @defer.inlineCallbacks - def _ask_id_server_for_third_party_invite( - self, id_server, medium, address, room_id, sender): - is_url = "https://%s/_matrix/identity/api/v1/nonce-it-up" % (id_server,) - data = yield self.hs.get_simple_http_client().post_urlencoded_get_json( - is_url, - { - "medium": medium, - "address": address, - "room_id": room_id, - "sender": sender, - } - ) - # TODO: Check for success - token = data["token"] - public_key = data["public_key"] - key_validity_url = "https://%s/_matrix/identity/api/v1/pubkey/isvalid" % ( - id_server, - ) - defer.returnValue((token, public_key, key_validity_url)) - @defer.inlineCallbacks def on_PUT(self, request, room_id, membership_action, txn_id): try: -- cgit 1.4.1 From 643b5fcdc81d8f2e39a97dd1a94c78cd8769c613 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 15 Oct 2015 13:10:30 +0100 Subject: Look for keys on the right objects --- synapse/federation/federation_client.py | 4 +++- synapse/handlers/room.py | 4 ++-- synapse/util/third_party_invites.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index d974e920c3..f5b430e046 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -364,7 +364,9 @@ class FederationClient(FederationBase): args = {} if third_party_invites.join_has_third_party_invite(content): - args = third_party_invites.extract_join_keys(content) + args = third_party_invites.extract_join_keys( + content["third_party_invite"] + ) try: ret = yield self.transport_layer.make_join( destination, room_id, user_id, args diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index bbb457e823..6936cdc5fd 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -566,7 +566,7 @@ class RoomMemberHandler(BaseHandler): if invitee: # make sure it looks like a user ID; it'll throw if it's invalid. UserID.from_string(invitee) - yield self.handlers.message_handler.create_and_send_event( + yield self.hs.get_handlers().message_handler.create_and_send_event( { "type": EventTypes.Member, "content": { @@ -641,7 +641,7 @@ class RoomMemberHandler(BaseHandler): user.to_string() ) ) - msg_handler = self.handlers.message_handler + msg_handler = self.hs.get_handlers().message_handler yield msg_handler.create_and_send_event( { "type": EventTypes.ThirdPartyInvite, diff --git a/synapse/util/third_party_invites.py b/synapse/util/third_party_invites.py index 45ae31d80c..41e597d5b9 100644 --- a/synapse/util/third_party_invites.py +++ b/synapse/util/third_party_invites.py @@ -51,7 +51,7 @@ def join_has_third_party_invite(content): def extract_join_keys(src): return { key: value - for key, value in src["third_party_invite"].items() + for key, value in src.items() if key in JOIN_KEYS } -- cgit 1.4.1 From 6ffbcf45c6b8472c2cd6095a9dca2bd11a027b44 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 15 Oct 2015 13:12:52 +0100 Subject: Use non-placeholder name for endpoint --- synapse/handlers/room.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 6936cdc5fd..9ffa521aad 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -661,7 +661,7 @@ class RoomMemberHandler(BaseHandler): @defer.inlineCallbacks def _ask_id_server_for_third_party_invite( self, id_server, medium, address, room_id, sender): - is_url = "https://%s/_matrix/identity/api/v1/nonce-it-up" % (id_server,) + is_url = "https://%s/_matrix/identity/api/v1/store-invite" % (id_server,) data = yield self.hs.get_simple_http_client().post_urlencoded_get_json( is_url, { -- cgit 1.4.1 From b8dd5b1a2d76f0426c600ae19ea9d9612e5327dc Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Fri, 16 Oct 2015 14:54:54 +0100 Subject: Verify third party ID server certificates --- synapse/api/auth.py | 11 +++++++++++ synapse/handlers/room.py | 31 +++++++++++++++++++++++++++++-- synapse/http/client.py | 4 ---- synapse/util/third_party_invites.py | 6 +++--- 4 files changed, 43 insertions(+), 9 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index e96d747b99..aee9b8a14f 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -397,13 +397,24 @@ class Auth(object): (EventTypes.ThirdPartyInvite, token,) ) if not invite_event: + logger.info("Failing 3pid invite because no invite found for token %s", token) return False try: public_key = join_third_party_invite["public_key"] key_validity_url = join_third_party_invite["key_validity_url"] if invite_event.content["public_key"] != public_key: + logger.info( + "Failing 3pid invite because public key invite: %s != join: %s", + invite_event.content["public_key"], + public_key + ) return False if invite_event.content["key_validity_url"] != key_validity_url: + logger.info( + "Failing 3pid invite because key_validity_url invite: %s != join: %s", + invite_event.content["key_validity_url"], + key_validity_url + ) return False verify_key = nacl.signing.VerifyKey(decode_base64(public_key)) encoded_signature = join_third_party_invite["signature"] diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 9ffa521aad..3f0cde56f0 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -22,11 +22,16 @@ from synapse.types import UserID, RoomAlias, RoomID from synapse.api.constants import ( EventTypes, Membership, JoinRules, RoomCreationPreset, ) -from synapse.api.errors import StoreError, SynapseError +from synapse.api.errors import AuthError, StoreError, SynapseError from synapse.util import stringutils, unwrapFirstError from synapse.util.async import run_on_reactor +from signedjson.sign import verify_signed_json +from signedjson.key import decode_verify_key_bytes + from collections import OrderedDict +from unpaddedbase64 import decode_base64 + import logging import string @@ -614,12 +619,34 @@ class RoomMemberHandler(BaseHandler): ) if "mxid" in data: - # TODO: Validate the response signature and such + if "signatures" not in data: + raise AuthError(401, "No signatures on 3pid binding") + self.verify_any_signature(data, id_server) defer.returnValue(data["mxid"]) + except IOError as e: logger.warn("Error from identity server lookup: %s" % (e,)) defer.returnValue(None) + @defer.inlineCallbacks + def verify_any_signature(self, data, server_hostname): + if server_hostname not in data["signatures"]: + raise AuthError(401, "No signature from server %s" % (server_hostname,)) + for key_name, signature in data["signatures"][server_hostname].items(): + key_data = yield self.hs.get_simple_http_client().get_json( + "https://%s/_matrix/identity/api/v1/pubkey/%s" % + (server_hostname, key_name,), + ) + if "public_key" not in key_data: + raise AuthError(401, "No public key named %s from %s" % + (key_name, server_hostname,)) + verify_signed_json( + data, + server_hostname, + decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"])) + ) + return + @defer.inlineCallbacks def _make_and_store_3pid_invite( self, diff --git a/synapse/http/client.py b/synapse/http/client.py index 9a5869abee..27e5190224 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -24,7 +24,6 @@ from canonicaljson import encode_canonical_json from twisted.internet import defer, reactor, ssl from twisted.web.client import ( Agent, readBody, FileBodyProducer, PartialDownloadError, - HTTPConnectionPool, ) from twisted.web.http_headers import Headers @@ -59,11 +58,8 @@ class SimpleHttpClient(object): # The default context factory in Twisted 14.0.0 (which we require) is # BrowserLikePolicyForHTTPS which will do regular cert validation # 'like a browser' - pool = HTTPConnectionPool(reactor) - pool.maxPersistentPerHost = 10 self.agent = Agent( reactor, - pool=pool, connectTimeout=15, contextFactory=hs.get_http_client_context_factory() ) diff --git a/synapse/util/third_party_invites.py b/synapse/util/third_party_invites.py index 41e597d5b9..335a9755b2 100644 --- a/synapse/util/third_party_invites.py +++ b/synapse/util/third_party_invites.py @@ -63,7 +63,7 @@ def check_key_valid(http_client, event): event.content["third_party_invite"]["key_validity_url"], {"public_key": event.content["third_party_invite"]["public_key"]} ) - if not response["valid"]: - raise AuthError(403, "Third party certificate was invalid") - except IOError: + except Exception: raise AuthError(502, "Third party certificate could not be checked") + if "valid" not in response or not response["valid"]: + raise AuthError(403, "Third party certificate was invalid") -- cgit 1.4.1 From 137fafce4ee06e76b05d37807611e10055059f62 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Tue, 20 Oct 2015 11:58:58 +0100 Subject: Allow rejecting invites This is done by using the same /leave flow as you would use if you had already accepted the invite and wanted to leave. --- synapse/api/auth.py | 6 +- synapse/federation/federation_client.py | 67 +++++++++- synapse/federation/federation_server.py | 14 +++ synapse/federation/transport/client.py | 24 +++- synapse/federation/transport/server.py | 20 +++ synapse/handlers/federation.py | 209 +++++++++++++++++++++++++------- synapse/handlers/room.py | 102 +++++++++------- tests/rest/client/v1/test_rooms.py | 4 +- 8 files changed, 353 insertions(+), 93 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index cf19eda4e9..494c8ac3d4 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -308,7 +308,11 @@ class Auth(object): ) if Membership.JOIN != membership: - # JOIN is the only action you can perform if you're not in the room + if (caller_invited + and Membership.LEAVE == membership + and target_user_id == event.user_id): + return True + if not caller_in_room: # caller isn't joined raise AuthError( 403, diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index f5b430e046..723f571284 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -17,6 +17,7 @@ from twisted.internet import defer from .federation_base import FederationBase +from synapse.api.constants import Membership from .units import Edu from synapse.api.errors import ( @@ -357,7 +358,34 @@ class FederationClient(FederationBase): defer.returnValue(signed_auth) @defer.inlineCallbacks - def make_join(self, destinations, room_id, user_id, content): + def make_membership_event(self, destinations, room_id, user_id, membership, content): + """ + Creates an m.room.member event, with context, without participating in the room. + + Does so by asking one of the already participating servers to create an + event with proper context. + + Note that this does not append any events to any graphs. + + Args: + destinations (str): Candidate homeservers which are probably + participating in the room. + room_id (str): The room in which the event will happen. + user_id (str): The user whose membership is being evented. + membership (str): The "membership" property of the event. Must be + one of "join" or "leave". + content (object): Any additional data to put into the content field + of the event. + Return: + A tuple of (origin (str), event (object)) where origin is the remote + homeserver which generated the event. + """ + valid_memberships = {Membership.JOIN, Membership.LEAVE} + if membership not in valid_memberships: + raise RuntimeError( + "make_membership_event called with membership='%s', must be one of %s" % + (membership, ",".join(valid_memberships)) + ) for destination in destinations: if destination == self.server_name: continue @@ -368,13 +396,13 @@ class FederationClient(FederationBase): content["third_party_invite"] ) try: - ret = yield self.transport_layer.make_join( - destination, room_id, user_id, args + ret = yield self.transport_layer.make_membership_event( + destination, room_id, user_id, membership, args ) pdu_dict = ret["event"] - logger.debug("Got response to make_join: %s", pdu_dict) + logger.debug("Got response to make_%s: %s", membership, pdu_dict) defer.returnValue( (destination, self.event_from_pdu_json(pdu_dict)) @@ -384,8 +412,8 @@ class FederationClient(FederationBase): raise except Exception as e: logger.warn( - "Failed to make_join via %s: %s", - destination, e.message + "Failed to make_%s via %s: %s", + membership, destination, e.message ) raise RuntimeError("Failed to send to any server.") @@ -491,6 +519,33 @@ class FederationClient(FederationBase): defer.returnValue(pdu) + @defer.inlineCallbacks + def send_leave(self, destinations, pdu): + for destination in destinations: + if destination == self.server_name: + continue + + try: + time_now = self._clock.time_msec() + _, content = yield self.transport_layer.send_leave( + destination=destination, + room_id=pdu.room_id, + event_id=pdu.event_id, + content=pdu.get_pdu_json(time_now), + ) + + logger.debug("Got content: %s", content) + defer.returnValue(None) + except CodeMessageException: + raise + except Exception as e: + logger.exception( + "Failed to send_leave via %s: %s", + destination, e.message + ) + + raise RuntimeError("Failed to send to any server.") + @defer.inlineCallbacks def query_auth(self, destination, room_id, event_id, local_auth): """ diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 7934f740e0..9e2d9ee74c 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -267,6 +267,20 @@ class FederationServer(FederationBase): ], })) + @defer.inlineCallbacks + def on_make_leave_request(self, room_id, user_id): + pdu = yield self.handler.on_make_leave_request(room_id, user_id) + time_now = self._clock.time_msec() + defer.returnValue({"event": pdu.get_pdu_json(time_now)}) + + @defer.inlineCallbacks + def on_send_leave_request(self, origin, content): + logger.debug("on_send_leave_request: content: %s", content) + pdu = self.event_from_pdu_json(content) + logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) + yield self.handler.on_send_leave_request(origin, pdu) + defer.returnValue((200, {})) + @defer.inlineCallbacks def on_event_auth(self, origin, room_id, event_id): time_now = self._clock.time_msec() diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index ae4195e83a..a81b3c4345 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -14,6 +14,7 @@ # limitations under the License. from twisted.internet import defer +from synapse.api.constants import Membership from synapse.api.urls import FEDERATION_PREFIX as PREFIX from synapse.util.logutils import log_function @@ -160,8 +161,14 @@ class TransportLayerClient(object): @defer.inlineCallbacks @log_function - def make_join(self, destination, room_id, user_id, args={}): - path = PREFIX + "/make_join/%s/%s" % (room_id, user_id) + def make_membership_event(self, destination, room_id, user_id, membership, args={}): + valid_memberships = {Membership.JOIN, Membership.LEAVE} + if membership not in valid_memberships: + raise RuntimeError( + "make_membership_event called with membership='%s', must be one of %s" % + (membership, ",".join(valid_memberships)) + ) + path = PREFIX + "/make_%s/%s/%s" % (membership, room_id, user_id) content = yield self.client.get_json( destination=destination, @@ -185,6 +192,19 @@ class TransportLayerClient(object): defer.returnValue(response) + @defer.inlineCallbacks + @log_function + def send_leave(self, destination, room_id, event_id, content): + path = PREFIX + "/send_leave/%s/%s" % (room_id, event_id) + + response = yield self.client.put_json( + destination=destination, + path=path, + data=content, + ) + + defer.returnValue(response) + @defer.inlineCallbacks @log_function def send_invite(self, destination, room_id, event_id, content): diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 6e394f039e..8184159210 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -296,6 +296,24 @@ class FederationMakeJoinServlet(BaseFederationServlet): defer.returnValue((200, content)) +class FederationMakeLeaveServlet(BaseFederationServlet): + PATH = "/make_leave/([^/]*)/([^/]*)" + + @defer.inlineCallbacks + def on_GET(self, origin, content, query, context, user_id): + content = yield self.handler.on_make_leave_request(context, user_id) + defer.returnValue((200, content)) + + +class FederationSendLeaveServlet(BaseFederationServlet): + PATH = "/send_leave/([^/]*)/([^/]*)" + + @defer.inlineCallbacks + def on_PUT(self, origin, content, query, room_id, txid): + content = yield self.handler.on_send_leave_request(origin, content) + defer.returnValue((200, content)) + + class FederationEventAuthServlet(BaseFederationServlet): PATH = "/event_auth/([^/]*)/([^/]*)" @@ -385,8 +403,10 @@ SERVLET_CLASSES = ( FederationBackfillServlet, FederationQueryServlet, FederationMakeJoinServlet, + FederationMakeLeaveServlet, FederationEventServlet, FederationSendJoinServlet, + FederationSendLeaveServlet, FederationInviteServlet, FederationQueryAuthServlet, FederationGetMissingEventsServlet, diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 946ff97c7d..ae9d227586 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -565,7 +565,7 @@ class FederationHandler(BaseHandler): @log_function @defer.inlineCallbacks - def do_invite_join(self, target_hosts, room_id, joinee, content, snapshot): + def do_invite_join(self, target_hosts, room_id, joinee, content): """ Attempts to join the `joinee` to the room `room_id` via the server `target_host`. @@ -581,50 +581,19 @@ class FederationHandler(BaseHandler): yield self.store.clean_room_for_join(room_id) - origin, pdu = yield self.replication_layer.make_join( + origin, event = yield self._make_and_verify_event( target_hosts, room_id, joinee, + "join", content ) - logger.debug("Got response to make_join: %s", pdu) - - event = pdu - - # We should assert some things. - # FIXME: Do this in a nicer way - assert(event.type == EventTypes.Member) - assert(event.user_id == joinee) - assert(event.state_key == joinee) - assert(event.room_id == room_id) - - event.internal_metadata.outlier = False - self.room_queues[room_id] = [] - - builder = self.event_builder_factory.new( - unfreeze(event.get_pdu_json()) - ) - handled_events = set() try: - builder.event_id = self.event_builder_factory.create_event_id() - builder.origin = self.hs.hostname - builder.content = content - - if not hasattr(event, "signatures"): - builder.signatures = {} - - add_hashes_and_signatures( - builder, - self.hs.hostname, - self.hs.config.signing_key[0], - ) - - new_event = builder.build() - + new_event = self._sign_event(event) # Try the host we successfully got a response to /make_join/ # request first. try: @@ -632,11 +601,7 @@ class FederationHandler(BaseHandler): target_hosts.insert(0, origin) except ValueError: pass - - ret = yield self.replication_layer.send_join( - target_hosts, - new_event - ) + ret = yield self.replication_layer.send_join(target_hosts, new_event) origin = ret["origin"] state = ret["state"] @@ -700,7 +665,7 @@ class FederationHandler(BaseHandler): @log_function def on_make_join_request(self, room_id, user_id, query): """ We've received a /make_join/ request, so we create a partial - join event for the room and return that. We don *not* persist or + join event for the room and return that. We do *not* persist or process it until the other server has signed it and sent it back. """ event_content = {"membership": Membership.JOIN} @@ -859,6 +824,168 @@ class FederationHandler(BaseHandler): defer.returnValue(event) + @defer.inlineCallbacks + def do_remotely_reject_invite(self, target_hosts, room_id, user_id): + origin, event = yield self._make_and_verify_event( + target_hosts, + room_id, + user_id, + "leave", + {} + ) + signed_event = self._sign_event(event) + + # Try the host we successfully got a response to /make_join/ + # request first. + try: + target_hosts.remove(origin) + target_hosts.insert(0, origin) + except ValueError: + pass + + yield self.replication_layer.send_leave( + target_hosts, + signed_event + ) + defer.returnValue(None) + + @defer.inlineCallbacks + def _make_and_verify_event(self, target_hosts, room_id, user_id, membership, content): + origin, pdu = yield self.replication_layer.make_membership_event( + target_hosts, + room_id, + user_id, + membership, + content + ) + + logger.debug("Got response to make_%s: %s", membership, pdu) + + event = pdu + + # We should assert some things. + # FIXME: Do this in a nicer way + assert(event.type == EventTypes.Member) + assert(event.user_id == user_id) + assert(event.state_key == user_id) + assert(event.room_id == room_id) + defer.returnValue((origin, event)) + + def _sign_event(self, event): + event.internal_metadata.outlier = False + + builder = self.event_builder_factory.new( + unfreeze(event.get_pdu_json()) + ) + + builder.event_id = self.event_builder_factory.create_event_id() + builder.origin = self.hs.hostname + + if not hasattr(event, "signatures"): + builder.signatures = {} + + add_hashes_and_signatures( + builder, + self.hs.hostname, + self.hs.config.signing_key[0], + ) + + return builder.build() + + @defer.inlineCallbacks + @log_function + def on_make_leave_request(self, room_id, user_id): + """ We've received a /make_leave/ request, so we create a partial + join event for the room and return that. We do *not* persist or + process it until the other server has signed it and sent it back. + """ + builder = self.event_builder_factory.new({ + "type": EventTypes.Member, + "content": {"membership": Membership.LEAVE}, + "room_id": room_id, + "sender": user_id, + "state_key": user_id, + }) + + event, context = yield self._create_new_client_event( + builder=builder, + ) + + self.auth.check(event, auth_events=context.current_state) + + defer.returnValue(event) + + @defer.inlineCallbacks + @log_function + def on_send_leave_request(self, origin, pdu): + """ We have received a leave event for a room. Fully process it.""" + event = pdu + + logger.debug( + "on_send_leave_request: Got event: %s, signatures: %s", + event.event_id, + event.signatures, + ) + + event.internal_metadata.outlier = False + + context, event_stream_id, max_stream_id = yield self._handle_new_event( + origin, event + ) + + logger.debug( + "on_send_leave_request: After _handle_new_event: %s, sigs: %s", + event.event_id, + event.signatures, + ) + + extra_users = [] + if event.type == EventTypes.Member: + target_user_id = event.state_key + target_user = UserID.from_string(target_user_id) + extra_users.append(target_user) + + with PreserveLoggingContext(): + d = self.notifier.on_new_room_event( + event, event_stream_id, max_stream_id, extra_users=extra_users + ) + + def log_failure(f): + logger.warn( + "Failed to notify about %s: %s", + event.event_id, f.value + ) + + d.addErrback(log_failure) + + new_pdu = event + + destinations = set() + + for k, s in context.current_state.items(): + try: + if k[0] == EventTypes.Member: + if s.content["membership"] == Membership.LEAVE: + destinations.add( + UserID.from_string(s.state_key).domain + ) + except: + logger.warn( + "Failed to get destination from event %s", s.event_id + ) + + destinations.discard(origin) + + logger.debug( + "on_send_leave_request: Sending event: %s, signatures: %s", + event.event_id, + event.signatures, + ) + + self.replication_layer.send_pdu(new_pdu, destinations) + + defer.returnValue(None) + @defer.inlineCallbacks def get_state_for_pdu(self, origin, room_id, event_id, do_auth=True): yield run_on_reactor() diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 3f0cde56f0..60f9fa58b0 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -389,7 +389,22 @@ class RoomMemberHandler(BaseHandler): if event.membership == Membership.JOIN: yield self._do_join(event, context, do_auth=do_auth) else: - # This is not a JOIN, so we can handle it normally. + if event.membership == Membership.LEAVE: + is_host_in_room = yield self.is_host_in_room(room_id, context) + if not is_host_in_room: + # Rejecting an invite, rather than leaving a joined room + handler = self.hs.get_handlers().federation_handler + inviter = yield self.get_inviter(event) + if not inviter: + # return the same error as join_room_alias does + raise SynapseError(404, "No known servers") + yield handler.do_remotely_reject_invite( + [inviter.domain], + room_id, + event.user_id + ) + defer.returnValue({"room_id": room_id}) + return # FIXME: This isn't idempotency. if prev_state and prev_state.membership == event.membership: @@ -413,7 +428,7 @@ class RoomMemberHandler(BaseHandler): defer.returnValue({"room_id": room_id}) @defer.inlineCallbacks - def join_room_alias(self, joinee, room_alias, do_auth=True, content={}): + def join_room_alias(self, joinee, room_alias, content={}): directory_handler = self.hs.get_handlers().directory_handler mapping = yield directory_handler.get_association(room_alias) @@ -447,8 +462,6 @@ class RoomMemberHandler(BaseHandler): @defer.inlineCallbacks def _do_join(self, event, context, room_hosts=None, do_auth=True): - joinee = UserID.from_string(event.state_key) - # room_id = RoomID.from_string(event.room_id, self.hs) room_id = event.room_id # XXX: We don't do an auth check if we are doing an invite @@ -456,48 +469,18 @@ class RoomMemberHandler(BaseHandler): # that we are allowed to join when we decide whether or not we # need to do the invite/join dance. - is_host_in_room = yield self.auth.check_host_in_room( - event.room_id, - self.hs.hostname - ) - if not is_host_in_room: - # is *anyone* in the room? - room_member_keys = [ - v for (k, v) in context.current_state.keys() if ( - k == "m.room.member" - ) - ] - if len(room_member_keys) == 0: - # has the room been created so we can join it? - create_event = context.current_state.get(("m.room.create", "")) - if create_event: - is_host_in_room = True - + is_host_in_room = yield self.is_host_in_room(room_id, context) if is_host_in_room: should_do_dance = False elif room_hosts: # TODO: Shouldn't this be remote_room_host? should_do_dance = True else: - # TODO(markjh): get prev_state from snapshot - prev_state = yield self.store.get_room_member( - joinee.to_string(), room_id - ) - - if prev_state and prev_state.membership == Membership.INVITE: - inviter = UserID.from_string(prev_state.user_id) - - should_do_dance = not self.hs.is_mine(inviter) - room_hosts = [inviter.domain] - elif "third_party_invite" in event.content: - if "sender" in event.content["third_party_invite"]: - inviter = UserID.from_string( - event.content["third_party_invite"]["sender"] - ) - should_do_dance = not self.hs.is_mine(inviter) - room_hosts = [inviter.domain] - else: + inviter = yield self.get_inviter(event) + if not inviter: # return the same error as join_room_alias does raise SynapseError(404, "No known servers") + should_do_dance = not self.hs.is_mine(inviter) + room_hosts = [inviter.domain] if should_do_dance: handler = self.hs.get_handlers().federation_handler @@ -505,8 +488,7 @@ class RoomMemberHandler(BaseHandler): room_hosts, room_id, event.user_id, - event.content, # FIXME To get a non-frozen dict - context + event.content # FIXME To get a non-frozen dict ) else: logger.debug("Doing normal join") @@ -523,6 +505,44 @@ class RoomMemberHandler(BaseHandler): "user_joined_room", user=user, room_id=room_id ) + @defer.inlineCallbacks + def get_inviter(self, event): + # TODO(markjh): get prev_state from snapshot + prev_state = yield self.store.get_room_member( + event.user_id, event.room_id + ) + + if prev_state and prev_state.membership == Membership.INVITE: + defer.returnValue(UserID.from_string(prev_state.user_id)) + return + elif "third_party_invite" in event.content: + if "sender" in event.content["third_party_invite"]: + inviter = UserID.from_string( + event.content["third_party_invite"]["sender"] + ) + defer.returnValue(inviter) + defer.returnValue(None) + + @defer.inlineCallbacks + def is_host_in_room(self, room_id, context): + is_host_in_room = yield self.auth.check_host_in_room( + room_id, + self.hs.hostname + ) + if not is_host_in_room: + # is *anyone* in the room? + room_member_keys = [ + v for (k, v) in context.current_state.keys() if ( + k == "m.room.member" + ) + ] + if len(room_member_keys) == 0: + # has the room been created so we can join it? + create_event = context.current_state.get(("m.room.create", "")) + if create_event: + is_host_in_room = True + defer.returnValue(is_host_in_room) + @defer.inlineCallbacks def get_joined_rooms_for_user(self, user): """Returns a list of roomids that the user has any of the given diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index a2123be81b..93896dd076 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -277,10 +277,10 @@ class RoomPermissionsTestCase(RestTestCase): expect_code=403) # set [invite/join/left] of self, set [invite/join/left] of other, - # expect all 403s + # expect all 404s because room doesn't exist on any server for usr in [self.user_id, self.rmcreator_id]: yield self.join(room=room, user=usr, expect_code=404) - yield self.leave(room=room, user=usr, expect_code=403) + yield self.leave(room=room, user=usr, expect_code=404) @defer.inlineCallbacks def test_membership_private_room_perms(self): -- cgit 1.4.1 From 5cb298c934adf9c7f42d06ea1ac74ab2bc651c23 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Oct 2015 13:45:56 +0000 Subject: Add room context api --- synapse/handlers/__init__.py | 3 +- synapse/handlers/room.py | 42 ++++++++++++++++ synapse/rest/client/v1/room.py | 36 +++++++++++++ synapse/storage/stream.py | 111 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 190 insertions(+), 2 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index 87b4d381c7..6a2339f2eb 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -17,7 +17,7 @@ from synapse.appservice.scheduler import AppServiceScheduler from synapse.appservice.api import ApplicationServiceApi from .register import RegistrationHandler from .room import ( - RoomCreationHandler, RoomMemberHandler, RoomListHandler + RoomCreationHandler, RoomMemberHandler, RoomListHandler, RoomContextHandler, ) from .message import MessageHandler from .events import EventStreamHandler, EventHandler @@ -70,3 +70,4 @@ class Handlers(object): self.auth_handler = AuthHandler(hs) self.identity_handler = IdentityHandler(hs) self.search_handler = SearchHandler(hs) + self.room_context_handler = RoomContextHandler(hs) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 60f9fa58b0..dd93a5d04d 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -33,6 +33,7 @@ from collections import OrderedDict from unpaddedbase64 import decode_base64 import logging +import math import string logger = logging.getLogger(__name__) @@ -747,6 +748,47 @@ class RoomListHandler(BaseHandler): defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) +class RoomContextHandler(BaseHandler): + @defer.inlineCallbacks + def get_event_context(self, user, room_id, event_id, limit): + before_limit = math.floor(limit/2.) + after_limit = limit - before_limit + + now_token = yield self.hs.get_event_sources().get_current_token() + + results = yield self.store.get_events_around( + room_id, event_id, before_limit, after_limit + ) + + results["events_before"] = yield self._filter_events_for_client( + user.to_string(), results["events_before"] + ) + + results["events_after"] = yield self._filter_events_for_client( + user.to_string(), results["events_after"] + ) + + if results["events_after"]: + last_event_id = results["events_after"][-1].event_id + else: + last_event_id = event_id + + state = yield self.store.get_state_for_events( + [last_event_id], None + ) + results["state"] = state[last_event_id].values() + + results["start"] = now_token.copy_and_replace( + "room_key", results["start"] + ).to_string() + + results["end"] = now_token.copy_and_replace( + "room_key", results["end"] + ).to_string() + + defer.returnValue(results) + + class RoomEventSource(object): def __init__(self, hs): self.store = hs.get_datastore() diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 4cee1c1599..2dcaee86cd 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -397,6 +397,41 @@ class RoomTriggerBackfill(ClientV1RestServlet): defer.returnValue((200, res)) +class RoomEventContext(ClientV1RestServlet): + PATTERN = client_path_pattern( + "/rooms/(?P[^/]*)/context/(?P[^/]*)$" + ) + + def __init__(self, hs): + super(RoomEventContext, self).__init__(hs) + self.clock = hs.get_clock() + + @defer.inlineCallbacks + def on_GET(self, request, room_id, event_id): + user, _ = yield self.auth.get_user_by_req(request) + + limit = int(request.args.get("limit", [10])[0]) + + results = yield self.handlers.room_context_handler.get_event_context( + user, room_id, event_id, limit, + ) + + time_now = self.clock.time_msec() + results["events_before"] = [ + serialize_event(event, time_now) for event in results["events_before"] + ] + results["events_after"] = [ + serialize_event(event, time_now) for event in results["events_after"] + ] + results["state"] = [ + serialize_event(event, time_now) for event in results["state"] + ] + + logger.info("Responding with %r", results) + + defer.returnValue((200, results)) + + # TODO: Needs unit testing class RoomMembershipRestServlet(ClientV1RestServlet): @@ -628,3 +663,4 @@ def register_servlets(hs, http_server): RoomRedactEventRestServlet(hs).register(http_server) RoomTypingRestServlet(hs).register(http_server) SearchRestServlet(hs).register(http_server) + RoomEventContext(hs).register(http_server) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index 3cab06fdef..f2eecf52f9 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -23,7 +23,7 @@ paginate bacwards. This is implemented by keeping two ordering columns: stream_ordering and topological_ordering. Stream ordering is basically insertion/received order -(except for events from backfill requests). The topolgical_ordering is a +(except for events from backfill requests). The topological_ordering is a weak ordering of events based on the pdu graph. This means that we have to have two different types of tokens, depending on @@ -436,3 +436,112 @@ class StreamStore(SQLBaseStore): internal = event.internal_metadata internal.before = str(RoomStreamToken(topo, stream - 1)) internal.after = str(RoomStreamToken(topo, stream)) + + @defer.inlineCallbacks + def get_events_around(self, room_id, event_id, before_limit, after_limit): + results = yield self.runInteraction( + "get_events_around", self._get_events_around_txn, + room_id, event_id, before_limit, after_limit + ) + + events_before = yield self._get_events( + [e for e in results["before"]["event_ids"]], + get_prev_content=True + ) + + events_after = yield self._get_events( + [e for e in results["after"]["event_ids"]], + get_prev_content=True + ) + + defer.returnValue({ + "events_before": events_before, + "events_after": events_after, + "start": results["before"]["token"], + "end": results["after"]["token"], + }) + + def _get_events_around_txn(self, txn, room_id, event_id, before_limit, after_limit): + results = self._simple_select_one_txn( + txn, + "events", + keyvalues={ + "event_id": event_id, + "room_id": room_id, + }, + retcols=["stream_ordering", "topological_ordering"], + ) + + stream_ordering = results["stream_ordering"] + topological_ordering = results["topological_ordering"] + + query_before = ( + "SELECT topological_ordering, stream_ordering, event_id FROM events" + " WHERE room_id = ? AND (topological_ordering < ?" + " OR (topological_ordering = ? AND stream_ordering < ?))" + " ORDER BY topological_ordering DESC, stream_ordering DESC" + " LIMIT ?" + ) + + query_after = ( + "SELECT topological_ordering, stream_ordering, event_id FROM events" + " WHERE room_id = ? AND (topological_ordering > ?" + " OR (topological_ordering = ? AND stream_ordering > ?))" + " ORDER BY topological_ordering ASC, stream_ordering ASC" + " LIMIT ?" + ) + + txn.execute( + query_before, + ( + room_id, topological_ordering, topological_ordering, + stream_ordering, before_limit, + ) + ) + + rows = self.cursor_to_dict(txn) + events_before = [r["event_id"] for r in rows] + + if rows: + start_token = str(RoomStreamToken( + rows[0]["topological_ordering"], + rows[0]["stream_ordering"] - 1, + )) + else: + start_token = str(RoomStreamToken( + topological_ordering, + stream_ordering - 1, + )) + + txn.execute( + query_after, + ( + room_id, topological_ordering, topological_ordering, + stream_ordering, after_limit, + ) + ) + + rows = self.cursor_to_dict(txn) + events_after = [r["event_id"] for r in rows] + + if rows: + end_token = str(RoomStreamToken( + rows[-1]["topological_ordering"], + rows[-1]["stream_ordering"], + )) + else: + end_token = str(RoomStreamToken( + topological_ordering, + stream_ordering, + )) + + return { + "before": { + "event_ids": events_before, + "token": start_token, + }, + "after": { + "event_ids": events_after, + "token": end_token, + }, + } -- cgit 1.4.1 From 56dbcd1524d855e0bea2a77e371b1732f82a9492 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Oct 2015 14:05:50 +0000 Subject: Docs --- synapse/handlers/room.py | 13 +++++++++++++ synapse/storage/stream.py | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index dd93a5d04d..36878a6c20 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -751,6 +751,19 @@ class RoomListHandler(BaseHandler): class RoomContextHandler(BaseHandler): @defer.inlineCallbacks def get_event_context(self, user, room_id, event_id, limit): + """Retrieves events, pagination tokens and state around a given event + in a room. + + Args: + user (UserID) + room_id (str) + event_id (str) + limit (int): The maximum number of events to return in total + (excluding state). + + Returns: + dict + """ before_limit = math.floor(limit/2.) after_limit = limit - before_limit diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index f2eecf52f9..15d4c2bf68 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -439,6 +439,19 @@ class StreamStore(SQLBaseStore): @defer.inlineCallbacks def get_events_around(self, room_id, event_id, before_limit, after_limit): + """Retrieve events and pagination tokens around a given event in a + room. + + Args: + room_id (str) + event_id (str) + before_limit (int) + after_limit (int) + + Returns: + dict + """ + results = yield self.runInteraction( "get_events_around", self._get_events_around_txn, room_id, event_id, before_limit, after_limit @@ -462,6 +475,19 @@ class StreamStore(SQLBaseStore): }) def _get_events_around_txn(self, txn, room_id, event_id, before_limit, after_limit): + """Retrieves event_ids and pagination tokens around a given event in a + room. + + Args: + room_id (str) + event_id (str) + before_limit (int) + after_limit (int) + + Returns: + dict + """ + results = self._simple_select_one_txn( txn, "events", -- cgit 1.4.1 From 771ca56c886dd08f707447cfff70acd3ba73e98c Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Mon, 2 Nov 2015 15:31:57 +0000 Subject: Remove more unused parameters --- synapse/handlers/room.py | 1 - synapse/handlers/sync.py | 1 - synapse/storage/stream.py | 3 +-- tests/storage/test_redaction.py | 4 ---- tests/storage/test_stream.py | 4 ---- tests/utils.py | 2 +- 6 files changed, 2 insertions(+), 13 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 36878a6c20..9184dcd048 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -827,7 +827,6 @@ class RoomEventSource(object): user_id=user.to_string(), from_key=from_key, to_key=to_key, - room_id=None, limit=limit, ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index eaa14f38df..4054efe555 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -342,7 +342,6 @@ class SyncHandler(BaseHandler): sync_config.user.to_string(), from_key=since_token.room_key, to_key=now_token.room_key, - room_id=None, limit=timeline_limit + 1, ) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index 15d4c2bf68..c728013f4c 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -158,8 +158,7 @@ class StreamStore(SQLBaseStore): defer.returnValue(results) @log_function - def get_room_events_stream(self, user_id, from_key, to_key, room_id, - limit=0): + def get_room_events_stream(self, user_id, from_key, to_key, limit=0): current_room_membership_sql = ( "SELECT m.room_id FROM room_memberships as m " " INNER JOIN current_state_events as c" diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index b57006fcb4..dbf9700e6a 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -120,7 +120,6 @@ class RedactionTestCase(unittest.TestCase): self.u_alice.to_string(), start, end, - None, # Is currently ignored ) self.assertEqual(1, len(results)) @@ -149,7 +148,6 @@ class RedactionTestCase(unittest.TestCase): self.u_alice.to_string(), start, end, - None, # Is currently ignored ) self.assertEqual(1, len(results)) @@ -199,7 +197,6 @@ class RedactionTestCase(unittest.TestCase): self.u_alice.to_string(), start, end, - None, # Is currently ignored ) self.assertEqual(1, len(results)) @@ -228,7 +225,6 @@ class RedactionTestCase(unittest.TestCase): self.u_alice.to_string(), start, end, - None, # Is currently ignored ) self.assertEqual(1, len(results)) diff --git a/tests/storage/test_stream.py b/tests/storage/test_stream.py index a658a789aa..e5c2c5cc8e 100644 --- a/tests/storage/test_stream.py +++ b/tests/storage/test_stream.py @@ -68,7 +68,6 @@ class StreamStoreTestCase(unittest.TestCase): self.u_bob.to_string(), start, end, - None, # Is currently ignored ) self.assertEqual(1, len(results)) @@ -105,7 +104,6 @@ class StreamStoreTestCase(unittest.TestCase): self.u_alice.to_string(), start, end, - None, # Is currently ignored ) self.assertEqual(1, len(results)) @@ -147,7 +145,6 @@ class StreamStoreTestCase(unittest.TestCase): self.u_bob.to_string(), start, end, - None, # Is currently ignored ) # We should not get the message, as it happened *after* bob left. @@ -175,7 +172,6 @@ class StreamStoreTestCase(unittest.TestCase): self.u_bob.to_string(), start, end, - None, # Is currently ignored ) # We should not get the message, as it happened *after* bob left. diff --git a/tests/utils.py b/tests/utils.py index 4da51291a4..ca2c33cf8e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -335,7 +335,7 @@ class MemoryDataStore(object): ] def get_room_events_stream(self, user_id=None, from_key=None, to_key=None, - room_id=None, limit=0, with_feedback=False): + limit=0, with_feedback=False): return ([], from_key) # TODO def get_joined_hosts_for_room(self, room_id): -- cgit 1.4.1 From ca2f90742d5606f8fc5b7ddd3dd7244c377c1df8 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 5 Nov 2015 14:32:26 +0000 Subject: Open up /events to anonymous users for room events only Squash-merge of PR #345 from daniel/anonymousevents --- synapse/handlers/_base.py | 7 ++- synapse/handlers/events.py | 10 ++- synapse/handlers/message.py | 47 ++++++++++---- synapse/handlers/presence.py | 4 +- synapse/handlers/private_user_data.py | 2 +- synapse/handlers/receipts.py | 6 +- synapse/handlers/room.py | 11 +++- synapse/handlers/sync.py | 20 +++++- synapse/handlers/typing.py | 11 +--- synapse/notifier.py | 42 ++++++++++--- synapse/rest/client/v1/events.py | 13 +++- synapse/rest/client/v1/room.py | 6 +- synapse/storage/events.py | 2 + synapse/storage/room.py | 13 ++++ .../storage/schema/delta/25/history_visibility.sql | 26 ++++++++ synapse/storage/stream.py | 46 +++++++++++--- tests/handlers/test_presence.py | 71 ++++++++++++++++------ tests/handlers/test_typing.py | 30 +++++++-- tests/rest/client/v1/test_presence.py | 9 ++- tests/rest/client/v1/test_typing.py | 5 +- 20 files changed, 299 insertions(+), 82 deletions(-) create mode 100644 synapse/storage/schema/delta/25/history_visibility.sql (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 6873a4575d..a9e43052b7 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -47,7 +47,8 @@ class BaseHandler(object): self.event_builder_factory = hs.get_event_builder_factory() @defer.inlineCallbacks - def _filter_events_for_client(self, user_id, events, is_guest=False): + def _filter_events_for_client(self, user_id, events, is_guest=False, + require_all_visible_for_guests=True): # Assumes that user has at some point joined the room if not is_guest. def allowed(event, membership, visibility): @@ -100,7 +101,9 @@ class BaseHandler(object): if should_include: events_to_return.append(event) - if is_guest and len(events_to_return) < len(events): + if (require_all_visible_for_guests + and is_guest + and len(events_to_return) < len(events)): # This indicates that some events in the requested range were not # visible to guest users. To be safe, we reject the entire request, # so that we don't have to worry about interpreting visibility diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index 53c8ca3a26..0e4c0d4d06 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -100,7 +100,7 @@ class EventStreamHandler(BaseHandler): @log_function def get_stream(self, auth_user_id, pagin_config, timeout=0, as_client_event=True, affect_presence=True, - only_room_events=False): + only_room_events=False, room_id=None, is_guest=False): """Fetches the events stream for a given user. If `only_room_events` is `True` only room events will be returned. @@ -119,9 +119,15 @@ class EventStreamHandler(BaseHandler): # thundering herds on restart. timeout = random.randint(int(timeout*0.9), int(timeout*1.1)) + if is_guest: + yield self.distributor.fire( + "user_joined_room", user=auth_user, room_id=room_id + ) + events, tokens = yield self.notifier.get_events_for( auth_user, pagin_config, timeout, - only_room_events=only_room_events + only_room_events=only_room_events, + is_guest=is_guest, guest_room_id=room_id ) time_now = self.clock.time_msec() diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 687e1527f7..654ecd2b37 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -16,7 +16,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, Membership -from synapse.api.errors import SynapseError +from synapse.api.errors import SynapseError, AuthError, Codes from synapse.streams.config import PaginationConfig from synapse.events.utils import serialize_event from synapse.events.validator import EventValidator @@ -229,7 +229,7 @@ class MessageHandler(BaseHandler): @defer.inlineCallbacks def get_room_data(self, user_id=None, room_id=None, - event_type=None, state_key=""): + event_type=None, state_key="", is_guest=False): """ Get data from a room. Args: @@ -239,23 +239,42 @@ class MessageHandler(BaseHandler): Raises: SynapseError if something went wrong. """ - member_event = yield self.auth.check_user_was_in_room(room_id, user_id) + membership, membership_event_id = yield self._check_in_room_or_world_readable( + room_id, user_id, is_guest + ) - if member_event.membership == Membership.JOIN: + if membership == Membership.JOIN: data = yield self.state_handler.get_current_state( room_id, event_type, state_key ) - elif member_event.membership == Membership.LEAVE: + elif membership == Membership.LEAVE: key = (event_type, state_key) room_state = yield self.store.get_state_for_events( - [member_event.event_id], [key] + [membership_event_id], [key] ) - data = room_state[member_event.event_id].get(key) + data = room_state[membership_event_id].get(key) defer.returnValue(data) @defer.inlineCallbacks - def get_state_events(self, user_id, room_id): + def _check_in_room_or_world_readable(self, room_id, user_id, is_guest): + if is_guest: + visibility = yield self.state_handler.get_current_state( + room_id, EventTypes.RoomHistoryVisibility, "" + ) + if visibility.content["history_visibility"] == "world_readable": + defer.returnValue((Membership.JOIN, None)) + return + else: + raise AuthError( + 403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN + ) + else: + member_event = yield self.auth.check_user_was_in_room(room_id, user_id) + defer.returnValue((member_event.membership, member_event.event_id)) + + @defer.inlineCallbacks + def get_state_events(self, user_id, room_id, is_guest=False): """Retrieve all state events for a given room. If the user is joined to the room then return the current state. If the user has left the room return the state events from when they left. @@ -266,15 +285,17 @@ class MessageHandler(BaseHandler): Returns: A list of dicts representing state events. [{}, {}, {}] """ - member_event = yield self.auth.check_user_was_in_room(room_id, user_id) + membership, membership_event_id = yield self._check_in_room_or_world_readable( + room_id, user_id, is_guest + ) - if member_event.membership == Membership.JOIN: + if membership == Membership.JOIN: room_state = yield self.state_handler.get_current_state(room_id) - elif member_event.membership == Membership.LEAVE: + elif membership == Membership.LEAVE: room_state = yield self.store.get_state_for_events( - [member_event.event_id], None + [membership_event_id], None ) - room_state = room_state[member_event.event_id] + room_state = room_state[membership_event_id] now = self.clock.time_msec() defer.returnValue( diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index ce60642127..0b780cd528 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1142,8 +1142,9 @@ class PresenceEventSource(object): @defer.inlineCallbacks @log_function - def get_new_events_for_user(self, user, from_key, limit): + def get_new_events(self, user, from_key, room_ids=None, **kwargs): from_key = int(from_key) + room_ids = room_ids or [] presence = self.hs.get_handlers().presence_handler cachemap = presence._user_cachemap @@ -1161,7 +1162,6 @@ class PresenceEventSource(object): user_ids_to_check |= set( UserID.from_string(p["observed_user_id"]) for p in presence_list ) - room_ids = yield presence.get_joined_rooms_for_user(user) for room_id in set(room_ids) & set(presence._room_serials): if presence._room_serials[room_id] > from_key: joined = yield presence.get_joined_users_for_room_id(room_id) diff --git a/synapse/handlers/private_user_data.py b/synapse/handlers/private_user_data.py index 1778c71325..1abe45ed7b 100644 --- a/synapse/handlers/private_user_data.py +++ b/synapse/handlers/private_user_data.py @@ -24,7 +24,7 @@ class PrivateUserDataEventSource(object): return self.store.get_max_private_user_data_stream_id() @defer.inlineCallbacks - def get_new_events_for_user(self, user, from_key, limit): + def get_new_events(self, user, from_key, **kwargs): user_id = user.to_string() last_stream_id = from_key diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index a47ae3df42..973f4d5cae 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -164,17 +164,15 @@ class ReceiptEventSource(object): self.store = hs.get_datastore() @defer.inlineCallbacks - def get_new_events_for_user(self, user, from_key, limit): + def get_new_events(self, from_key, room_ids, **kwargs): from_key = int(from_key) to_key = yield self.get_current_key() if from_key == to_key: defer.returnValue(([], to_key)) - rooms = yield self.store.get_rooms_for_user(user.to_string()) - rooms = [room.room_id for room in rooms] events = yield self.store.get_linearized_receipts_for_rooms( - rooms, + room_ids, from_key=from_key, to_key=to_key, ) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 9184dcd048..736ffe9066 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -807,7 +807,14 @@ class RoomEventSource(object): self.store = hs.get_datastore() @defer.inlineCallbacks - def get_new_events_for_user(self, user, from_key, limit): + def get_new_events( + self, + user, + from_key, + limit, + room_ids, + is_guest, + ): # We just ignore the key for now. to_key = yield self.get_current_key() @@ -828,6 +835,8 @@ class RoomEventSource(object): from_key=from_key, to_key=to_key, limit=limit, + room_ids=room_ids, + is_guest=is_guest, ) defer.returnValue((events, end_key)) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 1c1ee34b1e..5294d96466 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -295,11 +295,16 @@ class SyncHandler(BaseHandler): typing_key = since_token.typing_key if since_token else "0" + rooms = yield self.store.get_rooms_for_user(sync_config.user.to_string()) + room_ids = [room.room_id for room in rooms] + typing_source = self.event_sources.sources["typing"] - typing, typing_key = yield typing_source.get_new_events_for_user( + typing, typing_key = yield typing_source.get_new_events( user=sync_config.user, from_key=typing_key, limit=sync_config.filter.ephemeral_limit(), + room_ids=room_ids, + is_guest=False, ) now_token = now_token.copy_and_replace("typing_key", typing_key) @@ -312,10 +317,13 @@ class SyncHandler(BaseHandler): receipt_key = since_token.receipt_key if since_token else "0" receipt_source = self.event_sources.sources["receipt"] - receipts, receipt_key = yield receipt_source.get_new_events_for_user( + receipts, receipt_key = yield receipt_source.get_new_events( user=sync_config.user, from_key=receipt_key, limit=sync_config.filter.ephemeral_limit(), + room_ids=room_ids, + # /sync doesn't support guest access, they can't get to this point in code + is_guest=False, ) now_token = now_token.copy_and_replace("receipt_key", receipt_key) @@ -360,11 +368,17 @@ class SyncHandler(BaseHandler): """ now_token = yield self.event_sources.get_current_token() + rooms = yield self.store.get_rooms_for_user(sync_config.user.to_string()) + room_ids = [room.room_id for room in rooms] + presence_source = self.event_sources.sources["presence"] - presence, presence_key = yield presence_source.get_new_events_for_user( + presence, presence_key = yield presence_source.get_new_events( user=sync_config.user, from_key=since_token.presence_key, limit=sync_config.filter.presence_limit(), + room_ids=room_ids, + # /sync doesn't support guest access, they can't get to this point in code + is_guest=False, ) now_token = now_token.copy_and_replace("presence_key", presence_key) diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index d7096aab8c..2846f3e6e8 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -246,17 +246,12 @@ class TypingNotificationEventSource(object): }, } - @defer.inlineCallbacks - def get_new_events_for_user(self, user, from_key, limit): + def get_new_events(self, from_key, room_ids, **kwargs): from_key = int(from_key) handler = self.handler() - joined_room_ids = ( - yield self.room_member_handler().get_joined_rooms_for_user(user) - ) - events = [] - for room_id in joined_room_ids: + for room_id in room_ids: if room_id not in handler._room_serials: continue if handler._room_serials[room_id] <= from_key: @@ -264,7 +259,7 @@ class TypingNotificationEventSource(object): events.append(self._make_event_for(room_id)) - defer.returnValue((events, handler._latest_room_serial)) + return events, handler._latest_room_serial def get_current_key(self): return self.handler()._latest_room_serial diff --git a/synapse/notifier.py b/synapse/notifier.py index b69da63d43..56c4c863b5 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -269,7 +269,7 @@ class Notifier(object): logger.exception("Failed to notify listener") @defer.inlineCallbacks - def wait_for_events(self, user, timeout, callback, + def wait_for_events(self, user, timeout, callback, room_ids=None, from_token=StreamToken("s0", "0", "0", "0", "0")): """Wait until the callback returns a non empty response or the timeout fires. @@ -279,11 +279,12 @@ class Notifier(object): if user_stream is None: appservice = yield self.store.get_app_service_by_user_id(user) current_token = yield self.event_sources.get_current_token() - rooms = yield self.store.get_rooms_for_user(user) - rooms = [room.room_id for room in rooms] + if room_ids is None: + rooms = yield self.store.get_rooms_for_user(user) + room_ids = [room.room_id for room in rooms] user_stream = _NotifierUserStream( user=user, - rooms=rooms, + rooms=room_ids, appservice=appservice, current_token=current_token, time_now_ms=self.clock.time_msec(), @@ -329,7 +330,8 @@ class Notifier(object): @defer.inlineCallbacks def get_events_for(self, user, pagination_config, timeout, - only_room_events=False): + only_room_events=False, + is_guest=False, guest_room_id=None): """ For the given user and rooms, return any new events for them. If there are no new events wait for up to `timeout` milliseconds for any new events to happen before returning. @@ -342,6 +344,16 @@ class Notifier(object): limit = pagination_config.limit + room_ids = [] + if is_guest: + # TODO(daniel): Deal with non-room events too + only_room_events = True + if guest_room_id: + room_ids = [guest_room_id] + else: + rooms = yield self.store.get_rooms_for_user(user.to_string()) + room_ids = [room.room_id for room in rooms] + @defer.inlineCallbacks def check_for_updates(before_token, after_token): if not after_token.is_after(before_token): @@ -357,9 +369,23 @@ class Notifier(object): continue if only_room_events and name != "room": continue - new_events, new_key = yield source.get_new_events_for_user( - user, getattr(from_token, keyname), limit, + new_events, new_key = yield source.get_new_events( + user=user, + from_key=getattr(from_token, keyname), + limit=limit, + is_guest=is_guest, + room_ids=room_ids, ) + + if is_guest: + room_member_handler = self.hs.get_handlers().room_member_handler + new_events = yield room_member_handler._filter_events_for_client( + user.to_string(), + new_events, + is_guest=is_guest, + require_all_visible_for_guests=False + ) + events.extend(new_events) end_token = end_token.copy_and_replace(keyname, new_key) @@ -369,7 +395,7 @@ class Notifier(object): defer.returnValue(None) result = yield self.wait_for_events( - user, timeout, check_for_updates, from_token=from_token + user, timeout, check_for_updates, room_ids=room_ids, from_token=from_token ) if result is None: diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py index 4073b0d2d1..3e1750d1a1 100644 --- a/synapse/rest/client/v1/events.py +++ b/synapse/rest/client/v1/events.py @@ -34,7 +34,15 @@ class EventStreamRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_GET(self, request): - auth_user, _, _ = yield self.auth.get_user_by_req(request) + auth_user, _, is_guest = yield self.auth.get_user_by_req( + request, + allow_guest=True + ) + room_id = None + if is_guest: + if "room_id" not in request.args: + raise SynapseError(400, "Guest users must specify room_id param") + room_id = request.args["room_id"][0] try: handler = self.handlers.event_stream_handler pagin_config = PaginationConfig.from_request(request) @@ -49,7 +57,8 @@ class EventStreamRestServlet(ClientV1RestServlet): chunk = yield handler.get_stream( auth_user.to_string(), pagin_config, timeout=timeout, - as_client_event=as_client_event + as_client_event=as_client_event, affect_presence=(not is_guest), + room_id=room_id, is_guest=is_guest ) except: logger.exception("Event stream failed") diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 0876e593c5..afb802baec 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -125,7 +125,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_GET(self, request, room_id, event_type, state_key): - user, _, _ = yield self.auth.get_user_by_req(request) + user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) msg_handler = self.handlers.message_handler data = yield msg_handler.get_room_data( @@ -133,6 +133,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet): room_id=room_id, event_type=event_type, state_key=state_key, + is_guest=is_guest, ) if not data: @@ -348,12 +349,13 @@ class RoomStateRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_GET(self, request, room_id): - user, _, _ = yield self.auth.get_user_by_req(request) + user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) handler = self.handlers.message_handler # Get all the current state for this room events = yield handler.get_state_events( room_id=room_id, user_id=user.to_string(), + is_guest=is_guest, ) defer.returnValue((200, events)) diff --git a/synapse/storage/events.py b/synapse/storage/events.py index e6c1abfc27..59c9987202 100644 --- a/synapse/storage/events.py +++ b/synapse/storage/events.py @@ -311,6 +311,8 @@ class EventsStore(SQLBaseStore): self._store_room_message_txn(txn, event) elif event.type == EventTypes.Redaction: self._store_redaction(txn, event) + elif event.type == EventTypes.RoomHistoryVisibility: + self._store_history_visibility_txn(txn, event) self._store_room_members_txn( txn, diff --git a/synapse/storage/room.py b/synapse/storage/room.py index 13441fcdce..1c79626736 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -202,6 +202,19 @@ class RoomStore(SQLBaseStore): txn, event, "content.body", event.content["body"] ) + def _store_history_visibility_txn(self, txn, event): + if hasattr(event, "content") and "history_visibility" in event.content: + sql = ( + "INSERT INTO history_visibility" + " (event_id, room_id, history_visibility)" + " VALUES (?, ?, ?)" + ) + txn.execute(sql, ( + event.event_id, + event.room_id, + event.content["history_visibility"] + )) + def _store_event_search_txn(self, txn, event, key, value): if isinstance(self.database_engine, PostgresEngine): sql = ( diff --git a/synapse/storage/schema/delta/25/history_visibility.sql b/synapse/storage/schema/delta/25/history_visibility.sql new file mode 100644 index 0000000000..9f387ed69f --- /dev/null +++ b/synapse/storage/schema/delta/25/history_visibility.sql @@ -0,0 +1,26 @@ +/* Copyright 2015 OpenMarket 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. + */ + +/* + * This is a manual index of history_visibility content of state events, + * so that we can join on them in SELECT statements. + */ +CREATE TABLE IF NOT EXISTS history_visibility( + id INTEGER PRIMARY KEY, + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + history_visibility TEXT NOT NULL, + UNIQUE (event_id) +); diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index c728013f4c..be8ba76aae 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -158,13 +158,40 @@ class StreamStore(SQLBaseStore): defer.returnValue(results) @log_function - def get_room_events_stream(self, user_id, from_key, to_key, limit=0): - current_room_membership_sql = ( - "SELECT m.room_id FROM room_memberships as m " - " INNER JOIN current_state_events as c" - " ON m.event_id = c.event_id AND c.state_key = m.user_id" - " WHERE m.user_id = ? AND m.membership = 'join'" - ) + def get_room_events_stream( + self, + user_id, + from_key, + to_key, + limit=0, + is_guest=False, + room_ids=None + ): + room_ids = room_ids or [] + room_ids = [r for r in room_ids] + if is_guest: + current_room_membership_sql = ( + "SELECT c.room_id FROM history_visibility AS h" + " INNER JOIN current_state_events AS c" + " ON h.event_id = c.event_id" + " WHERE c.room_id IN (%s) AND h.history_visibility = 'world_readable'" % ( + ",".join(map(lambda _: "?", room_ids)) + ) + ) + current_room_membership_args = room_ids + else: + current_room_membership_sql = ( + "SELECT m.room_id FROM room_memberships as m " + " INNER JOIN current_state_events as c" + " ON m.event_id = c.event_id AND c.state_key = m.user_id" + " WHERE m.user_id = ? AND m.membership = 'join'" + ) + current_room_membership_args = [user_id] + if room_ids: + current_room_membership_sql += " AND m.room_id in (%s)" % ( + ",".join(map(lambda _: "?", room_ids)) + ) + current_room_membership_args = [user_id] + room_ids # We also want to get any membership events about that user, e.g. # invites or leave notifications. @@ -173,6 +200,7 @@ class StreamStore(SQLBaseStore): "INNER JOIN current_state_events as c ON m.event_id = c.event_id " "WHERE m.user_id = ? " ) + membership_args = [user_id] if limit: limit = max(limit, MAX_STREAM_SIZE) @@ -199,7 +227,9 @@ class StreamStore(SQLBaseStore): } def f(txn): - txn.execute(sql, (False, user_id, user_id, from_id.stream, to_id.stream,)) + args = ([False] + current_room_membership_args + membership_args + + [from_id.stream, to_id.stream]) + txn.execute(sql, args) rows = self.cursor_to_dict(txn) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 29372d488a..10d4482cce 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -650,9 +650,30 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): {"presence": ONLINE} ) + # Apple sees self-reflection even without room_id + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=0, + ) + + self.assertEquals(self.event_source.get_current_key(), 1) + self.assertEquals(events, + [ + {"type": "m.presence", + "content": { + "user_id": "@apple:test", + "presence": ONLINE, + "last_active_ago": 0, + }}, + ], + msg="Presence event should be visible to self-reflection" + ) + # Apple sees self-reflection - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_apple, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=0, + room_ids=[self.room_id], ) self.assertEquals(self.event_source.get_current_key(), 1) @@ -684,8 +705,10 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): ) # Banana sees it because of presence subscription - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_banana, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_banana, + from_key=0, + room_ids=[self.room_id], ) self.assertEquals(self.event_source.get_current_key(), 1) @@ -702,8 +725,10 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): ) # Elderberry sees it because of same room - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_elderberry, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_elderberry, + from_key=0, + room_ids=[self.room_id], ) self.assertEquals(self.event_source.get_current_key(), 1) @@ -720,8 +745,10 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): ) # Durian is not in the room, should not see this event - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_durian, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_durian, + from_key=0, + room_ids=[], ) self.assertEquals(self.event_source.get_current_key(), 1) @@ -767,8 +794,9 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): "accepted": True}, ], presence) - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_apple, 1, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=1, ) self.assertEquals(self.event_source.get_current_key(), 2) @@ -858,8 +886,10 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): ) ) - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_apple, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=0, + room_ids=[self.room_id], ) self.assertEquals(self.event_source.get_current_key(), 1) @@ -905,8 +935,10 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): self.assertEquals(self.event_source.get_current_key(), 1) - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_apple, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=0, + room_ids=[self.room_id,] ) self.assertEquals(events, [ @@ -932,8 +964,10 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): self.assertEquals(self.event_source.get_current_key(), 2) - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_apple, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=0, + room_ids=[self.room_id,] ) self.assertEquals(events, [ @@ -966,8 +1000,9 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase): self.room_members.append(self.u_clementine) - (events, _) = yield self.event_source.get_new_events_for_user( - self.u_apple, 0, None + (events, _) = yield self.event_source.get_new_events( + user=self.u_apple, + from_key=0, ) self.assertEquals(self.event_source.get_current_key(), 1) diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 41bb08b7ca..2d7ba43561 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -187,7 +187,10 @@ class TypingNotificationsTestCase(unittest.TestCase): ]) self.assertEquals(self.event_source.get_current_key(), 1) - events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None) + events = yield self.event_source.get_new_events( + room_ids=[self.room_id], + from_key=0, + ) self.assertEquals( events[0], [ @@ -250,7 +253,10 @@ class TypingNotificationsTestCase(unittest.TestCase): ]) self.assertEquals(self.event_source.get_current_key(), 1) - events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None) + events = yield self.event_source.get_new_events( + room_ids=[self.room_id], + from_key=0 + ) self.assertEquals( events[0], [ @@ -306,7 +312,10 @@ class TypingNotificationsTestCase(unittest.TestCase): yield put_json.await_calls() self.assertEquals(self.event_source.get_current_key(), 1) - events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None) + events = yield self.event_source.get_new_events( + room_ids=[self.room_id], + from_key=0, + ) self.assertEquals( events[0], [ @@ -337,7 +346,10 @@ class TypingNotificationsTestCase(unittest.TestCase): self.on_new_event.reset_mock() self.assertEquals(self.event_source.get_current_key(), 1) - events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None) + events = yield self.event_source.get_new_events( + room_ids=[self.room_id], + from_key=0, + ) self.assertEquals( events[0], [ @@ -356,7 +368,10 @@ class TypingNotificationsTestCase(unittest.TestCase): ]) self.assertEquals(self.event_source.get_current_key(), 2) - events = yield self.event_source.get_new_events_for_user(self.u_apple, 1, None) + events = yield self.event_source.get_new_events( + room_ids=[self.room_id], + from_key=1, + ) self.assertEquals( events[0], [ @@ -383,7 +398,10 @@ class TypingNotificationsTestCase(unittest.TestCase): self.on_new_event.reset_mock() self.assertEquals(self.event_source.get_current_key(), 3) - events = yield self.event_source.get_new_events_for_user(self.u_apple, 0, None) + events = yield self.event_source.get_new_events( + room_ids=[self.room_id], + from_key=0, + ) self.assertEquals( events[0], [ diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 3e0f294630..7f29d73d95 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -47,7 +47,14 @@ class NullSource(object): def __init__(self, hs): pass - def get_new_events_for_user(self, user, from_key, limit): + def get_new_events( + self, + user, + from_key, + room_ids=None, + limit=None, + is_guest=None + ): return defer.succeed(([], from_key)) def get_current_key(self, direction='f'): diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 8433585616..61b9cc743b 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -116,7 +116,10 @@ class RoomTypingTestCase(RestTestCase): self.assertEquals(200, code) self.assertEquals(self.event_source.get_current_key(), 1) - events = yield self.event_source.get_new_events_for_user(self.user, 0, None) + events = yield self.event_source.get_new_events( + from_key=0, + room_ids=[self.room_id], + ) self.assertEquals( events[0], [ -- cgit 1.4.1 From 2cebe5354504b3baf987c08a5c0098602b38ff84 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Thu, 5 Nov 2015 16:43:19 +0000 Subject: Exchange 3pid invites for m.room.member invites --- synapse/api/auth.py | 73 ++++++++++++------------ synapse/federation/federation_client.py | 33 ++++++++--- synapse/federation/federation_server.py | 31 +++++------ synapse/federation/transport/client.py | 16 +++++- synapse/federation/transport/server.py | 39 ++++++++++++- synapse/handlers/_base.py | 11 ---- synapse/handlers/federation.py | 99 +++++++++++++++++++++++++++------ synapse/handlers/room.py | 19 ++++--- synapse/rest/client/v1/room.py | 20 +++---- synapse/util/third_party_invites.py | 69 ----------------------- 10 files changed, 230 insertions(+), 180 deletions(-) delete mode 100644 synapse/util/third_party_invites.py (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index dfbbc5a1cd..3e891a6193 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -24,7 +24,6 @@ from synapse.api.constants import EventTypes, Membership, JoinRules from synapse.api.errors import AuthError, Codes, SynapseError, EventSizeError from synapse.types import RoomID, UserID, EventID from synapse.util.logutils import log_function -from synapse.util import third_party_invites from unpaddedbase64 import decode_base64 import logging @@ -318,6 +317,11 @@ class Auth(object): } ) + if Membership.INVITE == membership and "third_party_invite" in event.content: + if not self._verify_third_party_invite(event, auth_events): + raise AuthError(403, "You are not invited to this room.") + return True + if Membership.JOIN != membership: if (caller_invited and Membership.LEAVE == membership @@ -361,8 +365,7 @@ class Auth(object): pass elif join_rule == JoinRules.INVITE: if not caller_in_room and not caller_invited: - if not self._verify_third_party_invite(event, auth_events): - raise AuthError(403, "You are not invited to this room.") + raise AuthError(403, "You are not invited to this room.") else: # TODO (erikj): may_join list # TODO (erikj): private rooms @@ -390,10 +393,10 @@ class Auth(object): def _verify_third_party_invite(self, event, auth_events): """ - Validates that the join event is authorized by a previous third-party invite. + Validates that the invite event is authorized by a previous third-party invite. - Checks that the public key, and keyserver, match those in the invite, - and that the join event has a signature issued using that public key. + Checks that the public key, and keyserver, match those in the third party invite, + and that the invite event has a signature issued using that public key. Args: event: The m.room.member join event being validated. @@ -404,35 +407,28 @@ class Auth(object): True if the event fulfills the expectations of a previous third party invite event. """ - if not third_party_invites.join_has_third_party_invite(event.content): + if "third_party_invite" not in event.content: + return False + if "signed" not in event.content["third_party_invite"]: return False - join_third_party_invite = event.content["third_party_invite"] - token = join_third_party_invite["token"] + signed = event.content["third_party_invite"]["signed"] + for key in {"mxid", "token"}: + if key not in signed: + return False + + token = signed["token"] + invite_event = auth_events.get( (EventTypes.ThirdPartyInvite, token,) ) if not invite_event: - logger.info("Failing 3pid invite because no invite found for token %s", token) + return False + + if event.user_id != invite_event.user_id: return False try: - public_key = join_third_party_invite["public_key"] - key_validity_url = join_third_party_invite["key_validity_url"] - if invite_event.content["public_key"] != public_key: - logger.info( - "Failing 3pid invite because public key invite: %s != join: %s", - invite_event.content["public_key"], - public_key - ) - return False - if invite_event.content["key_validity_url"] != key_validity_url: - logger.info( - "Failing 3pid invite because key_validity_url invite: %s != join: %s", - invite_event.content["key_validity_url"], - key_validity_url - ) - return False - signed = join_third_party_invite["signed"] - if signed["mxid"] != event.user_id: + public_key = invite_event.content["public_key"] + if signed["mxid"] != event.state_key: return False if signed["token"] != token: return False @@ -445,6 +441,11 @@ class Auth(object): decode_base64(public_key) ) verify_signed_json(signed, server, verify_key) + + # We got the public key from the invite, so we know that the + # correct server signed the signed bundle. + # The caller is responsible for checking that the signing + # server has not revoked that public key. return True return False except (KeyError, SignatureVerifyException,): @@ -751,17 +752,19 @@ class Auth(object): if e_type == Membership.JOIN: if member_event and not is_public: auth_ids.append(member_event.event_id) - if third_party_invites.join_has_third_party_invite(event.content): + else: + if member_event: + auth_ids.append(member_event.event_id) + + if e_type == Membership.INVITE: + if "third_party_invite" in event.content: key = ( EventTypes.ThirdPartyInvite, event.content["third_party_invite"]["token"] ) - invite = current_state.get(key) - if invite: - auth_ids.append(invite.event_id) - else: - if member_event: - auth_ids.append(member_event.event_id) + third_party_invite = current_state.get(key) + if third_party_invite: + auth_ids.append(third_party_invite.event_id) elif member_event: if member_event.content["membership"] == Membership.JOIN: auth_ids.append(member_event.event_id) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 723f571284..c0c0b693b8 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -26,7 +26,6 @@ from synapse.api.errors import ( from synapse.util import unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.logutils import log_function -from synapse.util import third_party_invites from synapse.events import FrozenEvent import synapse.metrics @@ -358,7 +357,7 @@ class FederationClient(FederationBase): defer.returnValue(signed_auth) @defer.inlineCallbacks - def make_membership_event(self, destinations, room_id, user_id, membership, content): + def make_membership_event(self, destinations, room_id, user_id, membership): """ Creates an m.room.member event, with context, without participating in the room. @@ -390,14 +389,9 @@ class FederationClient(FederationBase): if destination == self.server_name: continue - args = {} - if third_party_invites.join_has_third_party_invite(content): - args = third_party_invites.extract_join_keys( - content["third_party_invite"] - ) try: ret = yield self.transport_layer.make_membership_event( - destination, room_id, user_id, membership, args + destination, room_id, user_id, membership ) pdu_dict = ret["event"] @@ -704,3 +698,26 @@ class FederationClient(FederationBase): event.internal_metadata.outlier = outlier return event + + @defer.inlineCallbacks + def forward_third_party_invite(self, destinations, room_id, event_dict): + for destination in destinations: + if destination == self.server_name: + continue + + try: + yield self.transport_layer.exchange_third_party_invite( + destination=destination, + room_id=room_id, + event_dict=event_dict, + ) + defer.returnValue(None) + except CodeMessageException: + raise + except Exception as e: + logger.exception( + "Failed to send_third_party_invite via %s: %s", + destination, e.message + ) + + raise RuntimeError("Failed to send to any server.") diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 9e2d9ee74c..7a59436a91 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -23,12 +23,10 @@ from synapse.util.logutils import log_function from synapse.events import FrozenEvent import synapse.metrics -from synapse.api.errors import FederationError, SynapseError, Codes +from synapse.api.errors import FederationError, SynapseError from synapse.crypto.event_signing import compute_event_signature -from synapse.util import third_party_invites - import simplejson as json import logging @@ -230,19 +228,8 @@ class FederationServer(FederationBase): ) @defer.inlineCallbacks - def on_make_join_request(self, room_id, user_id, query): - threepid_details = {} - if third_party_invites.has_join_keys(query): - for k in third_party_invites.JOIN_KEYS: - if not isinstance(query[k], list) or len(query[k]) != 1: - raise FederationError( - "FATAL", - Codes.MISSING_PARAM, - "key %s value %s" % (k, query[k],), - None - ) - threepid_details[k] = query[k][0] - pdu = yield self.handler.on_make_join_request(room_id, user_id, threepid_details) + def on_make_join_request(self, room_id, user_id): + pdu = yield self.handler.on_make_join_request(room_id, user_id) time_now = self._clock.time_msec() defer.returnValue({"event": pdu.get_pdu_json(time_now)}) @@ -556,3 +543,15 @@ class FederationServer(FederationBase): event.internal_metadata.outlier = outlier return event + + @defer.inlineCallbacks + def exchange_third_party_invite(self, invite): + ret = yield self.handler.exchange_third_party_invite(invite) + defer.returnValue(ret) + + @defer.inlineCallbacks + def on_exchange_third_party_invite_request(self, origin, room_id, event_dict): + ret = yield self.handler.on_exchange_third_party_invite_request( + origin, room_id, event_dict + ) + defer.returnValue(ret) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index a81b3c4345..3d59e1c650 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -161,7 +161,7 @@ class TransportLayerClient(object): @defer.inlineCallbacks @log_function - def make_membership_event(self, destination, room_id, user_id, membership, args={}): + def make_membership_event(self, destination, room_id, user_id, membership): valid_memberships = {Membership.JOIN, Membership.LEAVE} if membership not in valid_memberships: raise RuntimeError( @@ -173,7 +173,6 @@ class TransportLayerClient(object): content = yield self.client.get_json( destination=destination, path=path, - args=args, retry_on_dns_fail=True, ) @@ -218,6 +217,19 @@ class TransportLayerClient(object): defer.returnValue(response) + @defer.inlineCallbacks + @log_function + def exchange_third_party_invite(self, destination, room_id, event_dict): + path = PREFIX + "/exchange_third_party_invite/%s" % (room_id,) + + response = yield self.client.put_json( + destination=destination, + path=path, + data=event_dict, + ) + + defer.returnValue(response) + @defer.inlineCallbacks @log_function def get_event_auth(self, destination, room_id, event_id): diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 8184159210..127b4da4f8 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -292,7 +292,7 @@ class FederationMakeJoinServlet(BaseFederationServlet): @defer.inlineCallbacks def on_GET(self, origin, content, query, context, user_id): - content = yield self.handler.on_make_join_request(context, user_id, query) + content = yield self.handler.on_make_join_request(context, user_id) defer.returnValue((200, content)) @@ -343,6 +343,17 @@ class FederationInviteServlet(BaseFederationServlet): defer.returnValue((200, content)) +class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet): + PATH = "/exchange_third_party_invite/([^/]*)" + + @defer.inlineCallbacks + def on_PUT(self, origin, content, query, room_id): + content = yield self.handler.on_exchange_third_party_invite_request( + origin, room_id, content + ) + defer.returnValue((200, content)) + + class FederationClientKeysQueryServlet(BaseFederationServlet): PATH = "/user/keys/query" @@ -396,6 +407,30 @@ class FederationGetMissingEventsServlet(BaseFederationServlet): defer.returnValue((200, content)) +class On3pidBindServlet(BaseFederationServlet): + PATH = "/3pid/onbind" + + @defer.inlineCallbacks + def on_POST(self, request): + content_bytes = request.content.read() + content = json.loads(content_bytes) + if "invites" in content: + last_exception = None + for invite in content["invites"]: + try: + yield self.handler.exchange_third_party_invite(invite) + except Exception as e: + last_exception = e + if last_exception: + raise last_exception + defer.returnValue((200, {})) + + # Avoid doing remote HS authorization checks which are done by default by + # BaseFederationServlet. + def _wrap(self, code): + return code + + SERVLET_CLASSES = ( FederationPullServlet, FederationEventServlet, @@ -413,4 +448,6 @@ SERVLET_CLASSES = ( FederationEventAuthServlet, FederationClientKeysQueryServlet, FederationClientKeysClaimServlet, + FederationThirdPartyInviteExchangeServlet, + On3pidBindServlet, ) diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index a9e43052b7..eef325a94b 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -21,7 +21,6 @@ from synapse.api.constants import Membership, EventTypes from synapse.types import UserID, RoomAlias from synapse.util.logcontext import PreserveLoggingContext -from synapse.util import third_party_invites import logging @@ -192,16 +191,6 @@ class BaseHandler(object): ) ) - if ( - event.type == EventTypes.Member and - event.content["membership"] == Membership.JOIN and - third_party_invites.join_has_third_party_invite(event.content) - ): - yield third_party_invites.check_key_valid( - self.hs.get_simple_http_client(), - event - ) - federation_handler = self.hs.get_handlers().federation_handler if event.type == EventTypes.Member: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index b2395b28d1..872051b8b9 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -21,6 +21,7 @@ from synapse.api.errors import ( AuthError, FederationError, StoreError, CodeMessageException, SynapseError, ) from synapse.api.constants import EventTypes, Membership, RejectedReason +from synapse.events.validator import EventValidator from synapse.util import unwrapFirstError from synapse.util.logcontext import PreserveLoggingContext from synapse.util.logutils import log_function @@ -39,7 +40,6 @@ from twisted.internet import defer import itertools import logging -from synapse.util import third_party_invites logger = logging.getLogger(__name__) @@ -58,6 +58,8 @@ class FederationHandler(BaseHandler): def __init__(self, hs): super(FederationHandler, self).__init__(hs) + self.hs = hs + self.distributor.observe( "user_joined_room", self._on_user_joined @@ -68,7 +70,6 @@ class FederationHandler(BaseHandler): self.store = hs.get_datastore() self.replication_layer = hs.get_replication_layer() self.state_handler = hs.get_state_handler() - # self.auth_handler = gs.get_auth_handler() self.server_name = hs.hostname self.keyring = hs.get_keyring() @@ -563,7 +564,7 @@ class FederationHandler(BaseHandler): @log_function @defer.inlineCallbacks - def do_invite_join(self, target_hosts, room_id, joinee, content): + def do_invite_join(self, target_hosts, room_id, joinee): """ Attempts to join the `joinee` to the room `room_id` via the server `target_host`. @@ -583,8 +584,7 @@ class FederationHandler(BaseHandler): target_hosts, room_id, joinee, - "join", - content + "join" ) self.room_queues[room_id] = [] @@ -661,16 +661,12 @@ class FederationHandler(BaseHandler): @defer.inlineCallbacks @log_function - def on_make_join_request(self, room_id, user_id, query): + def on_make_join_request(self, room_id, user_id): """ We've received a /make_join/ request, so we create a partial join event for the room and return that. We do *not* persist or process it until the other server has signed it and sent it back. """ event_content = {"membership": Membership.JOIN} - if third_party_invites.has_join_keys(query): - event_content["third_party_invite"] = ( - third_party_invites.extract_join_keys(query) - ) builder = self.event_builder_factory.new({ "type": EventTypes.Member, @@ -686,9 +682,6 @@ class FederationHandler(BaseHandler): self.auth.check(event, auth_events=context.current_state) - if third_party_invites.join_has_third_party_invite(event.content): - third_party_invites.check_key_valid(self.hs.get_simple_http_client(), event) - defer.returnValue(event) @defer.inlineCallbacks @@ -828,8 +821,7 @@ class FederationHandler(BaseHandler): target_hosts, room_id, user_id, - "leave", - {} + "leave" ) signed_event = self._sign_event(event) @@ -848,13 +840,12 @@ class FederationHandler(BaseHandler): defer.returnValue(None) @defer.inlineCallbacks - def _make_and_verify_event(self, target_hosts, room_id, user_id, membership, content): + def _make_and_verify_event(self, target_hosts, room_id, user_id, membership): origin, pdu = yield self.replication_layer.make_membership_event( target_hosts, room_id, user_id, - membership, - content + membership ) logger.debug("Got response to make_%s: %s", membership, pdu) @@ -1647,3 +1638,75 @@ class FederationHandler(BaseHandler): }, "missing": [e.event_id for e in missing_locals], }) + + @defer.inlineCallbacks + @log_function + def exchange_third_party_invite(self, invite): + sender = invite["sender"] + room_id = invite["room_id"] + + event_dict = { + "type": EventTypes.Member, + "content": { + "membership": Membership.INVITE, + "third_party_invite": invite, + }, + "room_id": room_id, + "sender": sender, + "state_key": invite["mxid"], + } + + if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)): + builder = self.event_builder_factory.new(event_dict) + EventValidator().validate_new(builder) + event, context = yield self._create_new_client_event(builder=builder) + self.auth.check(event, context.current_state) + yield self._validate_keyserver(event, auth_events=context.current_state) + member_handler = self.hs.get_handlers().room_member_handler + yield member_handler.change_membership(event, context) + else: + destinations = set([x.split(":", 1)[-1] for x in (sender, room_id)]) + yield self.replication_layer.forward_third_party_invite( + destinations, + room_id, + event_dict, + ) + + @defer.inlineCallbacks + @log_function + def on_exchange_third_party_invite_request(self, origin, room_id, event_dict): + builder = self.event_builder_factory.new(event_dict) + + event, context = yield self._create_new_client_event( + builder=builder, + ) + + self.auth.check(event, auth_events=context.current_state) + yield self._validate_keyserver(event, auth_events=context.current_state) + + returned_invite = yield self.send_invite(origin, event) + # TODO: Make sure the signatures actually are correct. + event.signatures.update(returned_invite.signatures) + member_handler = self.hs.get_handlers().room_member_handler + yield member_handler.change_membership(event, context) + + @defer.inlineCallbacks + def _validate_keyserver(self, event, auth_events): + token = event.content["third_party_invite"]["signed"]["token"] + + invite_event = auth_events.get( + (EventTypes.ThirdPartyInvite, token,) + ) + + try: + response = yield self.hs.get_simple_http_client().get_json( + invite_event.content["key_validity_url"], + {"public_key": invite_event.content["public_key"]} + ) + except Exception: + raise SynapseError( + 502, + "Third party certificate could not be checked" + ) + if "valid" not in response or not response["valid"]: + raise AuthError(403, "Third party certificate was invalid") diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 736ffe9066..8cce8d0e99 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -38,6 +38,8 @@ import string logger = logging.getLogger(__name__) +id_server_scheme = "https://" + class RoomCreationHandler(BaseHandler): @@ -488,8 +490,7 @@ class RoomMemberHandler(BaseHandler): yield handler.do_invite_join( room_hosts, room_id, - event.user_id, - event.content # FIXME To get a non-frozen dict + event.user_id ) else: logger.debug("Doing normal join") @@ -632,7 +633,7 @@ class RoomMemberHandler(BaseHandler): """ try: data = yield self.hs.get_simple_http_client().get_json( - "https://%s/_matrix/identity/api/v1/lookup" % (id_server,), + "%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server,), { "medium": medium, "address": address, @@ -655,8 +656,8 @@ class RoomMemberHandler(BaseHandler): raise AuthError(401, "No signature from server %s" % (server_hostname,)) for key_name, signature in data["signatures"][server_hostname].items(): key_data = yield self.hs.get_simple_http_client().get_json( - "https://%s/_matrix/identity/api/v1/pubkey/%s" % - (server_hostname, key_name,), + "%s%s/_matrix/identity/api/v1/pubkey/%s" % + (id_server_scheme, server_hostname, key_name,), ) if "public_key" not in key_data: raise AuthError(401, "No public key named %s from %s" % @@ -709,7 +710,9 @@ class RoomMemberHandler(BaseHandler): @defer.inlineCallbacks def _ask_id_server_for_third_party_invite( self, id_server, medium, address, room_id, sender): - is_url = "https://%s/_matrix/identity/api/v1/store-invite" % (id_server,) + is_url = "%s%s/_matrix/identity/api/v1/store-invite" % ( + id_server_scheme, id_server, + ) data = yield self.hs.get_simple_http_client().post_urlencoded_get_json( is_url, { @@ -722,8 +725,8 @@ class RoomMemberHandler(BaseHandler): # TODO: Check for success token = data["token"] public_key = data["public_key"] - key_validity_url = "https://%s/_matrix/identity/api/v1/pubkey/isvalid" % ( - id_server, + key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % ( + id_server_scheme, id_server, ) defer.returnValue((token, public_key, key_validity_url)) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index afb802baec..3628298376 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -26,7 +26,6 @@ from synapse.events.utils import serialize_event import simplejson as json import logging import urllib -from synapse.util import third_party_invites logger = logging.getLogger(__name__) @@ -453,7 +452,7 @@ class RoomMembershipRestServlet(ClientV1RestServlet): # target user is you unless it is an invite state_key = user.to_string() - if membership_action == "invite" and third_party_invites.has_invite_keys(content): + if membership_action == "invite" and self._has_3pid_invite_keys(content): yield self.handlers.room_member_handler.do_3pid_invite( room_id, user, @@ -480,19 +479,10 @@ class RoomMembershipRestServlet(ClientV1RestServlet): msg_handler = self.handlers.message_handler - event_content = { - "membership": unicode(membership_action), - } - - if membership_action == "join" and third_party_invites.has_join_keys(content): - event_content["third_party_invite"] = ( - third_party_invites.extract_join_keys(content) - ) - yield msg_handler.create_and_send_event( { "type": EventTypes.Member, - "content": event_content, + "content": {"membership": unicode(membership_action)}, "room_id": room_id, "sender": user.to_string(), "state_key": state_key, @@ -503,6 +493,12 @@ class RoomMembershipRestServlet(ClientV1RestServlet): defer.returnValue((200, {})) + def _has_3pid_invite_keys(self, content): + for key in {"id_server", "medium", "address", "display_name"}: + if key not in content: + return False + return True + @defer.inlineCallbacks def on_PUT(self, request, room_id, membership_action, txn_id): try: diff --git a/synapse/util/third_party_invites.py b/synapse/util/third_party_invites.py deleted file mode 100644 index 31d186740d..0000000000 --- a/synapse/util/third_party_invites.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2015 OpenMarket 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. - -from twisted.internet import defer -from synapse.api.errors import AuthError - - -INVITE_KEYS = {"id_server", "medium", "address", "display_name"} - -JOIN_KEYS = { - "token", - "public_key", - "key_validity_url", - "sender", - "signed", -} - - -def has_invite_keys(content): - for key in INVITE_KEYS: - if key not in content: - return False - return True - - -def has_join_keys(content): - for key in JOIN_KEYS: - if key not in content: - return False - return True - - -def join_has_third_party_invite(content): - if "third_party_invite" not in content: - return False - return has_join_keys(content["third_party_invite"]) - - -def extract_join_keys(src): - return { - key: value - for key, value in src.items() - if key in JOIN_KEYS - } - - -@defer.inlineCallbacks -def check_key_valid(http_client, event): - try: - response = yield http_client.get_json( - event.content["third_party_invite"]["key_validity_url"], - {"public_key": event.content["third_party_invite"]["public_key"]} - ) - except Exception: - raise AuthError(502, "Third party certificate could not be checked") - if "valid" not in response or not response["valid"]: - raise AuthError(403, "Third party certificate was invalid") -- cgit 1.4.1 From 0d63dc3ec983f9ae21eeedd4276b1ac767e9cc1b Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Mon, 9 Nov 2015 17:26:43 +0000 Subject: Get display name from identity server, not client --- synapse/handlers/room.py | 8 +++----- synapse/rest/client/v1/room.py | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 8cce8d0e99..834972a580 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -582,7 +582,6 @@ class RoomMemberHandler(BaseHandler): medium, address, id_server, - display_name, token_id, txn_id ): @@ -609,7 +608,6 @@ class RoomMemberHandler(BaseHandler): else: yield self._make_and_store_3pid_invite( id_server, - display_name, medium, address, room_id, @@ -673,7 +671,6 @@ class RoomMemberHandler(BaseHandler): def _make_and_store_3pid_invite( self, id_server, - display_name, medium, address, room_id, @@ -681,7 +678,7 @@ class RoomMemberHandler(BaseHandler): token_id, txn_id ): - token, public_key, key_validity_url = ( + token, public_key, key_validity_url, display_name = ( yield self._ask_id_server_for_third_party_invite( id_server, medium, @@ -725,10 +722,11 @@ class RoomMemberHandler(BaseHandler): # TODO: Check for success token = data["token"] public_key = data["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)) + defer.returnValue((token, public_key, key_validity_url, display_name)) class RoomListHandler(BaseHandler): diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 6e0d93766b..90ded26ff8 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -459,7 +459,6 @@ class RoomMembershipRestServlet(ClientV1RestServlet): content["medium"], content["address"], content["id_server"], - content["display_name"], token_id, txn_id ) @@ -494,7 +493,7 @@ class RoomMembershipRestServlet(ClientV1RestServlet): defer.returnValue((200, {})) def _has_3pid_invite_keys(self, content): - for key in {"id_server", "medium", "address", "display_name"}: + for key in {"id_server", "medium", "address"}: if key not in content: return False return True -- cgit 1.4.1 From 38d82edf0e463e1e6eb6859330f2517cc7ae3e41 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Tue, 10 Nov 2015 16:57:13 +0000 Subject: Allow guest users to join and message rooms --- synapse/api/constants.py | 1 + synapse/handlers/_base.py | 57 ++++++++++++++++++++++++++++++++++++++++++ synapse/handlers/federation.py | 10 ++++---- synapse/handlers/message.py | 4 +-- synapse/handlers/presence.py | 3 ++- synapse/handlers/room.py | 16 +++++++++++- synapse/rest/client/v1/room.py | 13 +++++++--- 7 files changed, 92 insertions(+), 12 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 41125e8719..c2450b771a 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -68,6 +68,7 @@ class EventTypes(object): RoomHistoryVisibility = "m.room.history_visibility" CanonicalAlias = "m.room.canonical_alias" RoomAvatar = "m.room.avatar" + GuestAccess = "m.room.guest_access" # These are used for validation Message = "m.room.message" diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index eef325a94b..f4ade1f594 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -175,6 +175,8 @@ class BaseHandler(object): if not suppress_auth: self.auth.check(event, auth_events=context.current_state) + yield self.maybe_kick_guest_users(event, context.current_state.values()) + if event.type == EventTypes.CanonicalAlias: # Check the alias is acually valid (at this time at least) room_alias_str = event.content.get("alias", None) @@ -282,3 +284,58 @@ class BaseHandler(object): federation_handler.handle_new_event( event, destinations=destinations, ) + + @defer.inlineCallbacks + def maybe_kick_guest_users(self, event, current_state): + # Technically this function invalidates current_state by changing it. + # Hopefully this isn't that important to the caller. + if event.type == EventTypes.GuestAccess: + guest_access = event.content.get("guest_access", "forbidden") + if guest_access != "can_join": + yield self.kick_guest_users(current_state) + + @defer.inlineCallbacks + def kick_guest_users(self, current_state): + for member_event in current_state: + try: + if member_event.type != EventTypes.Member: + continue + + if not self.hs.is_mine(UserID.from_string(member_event.state_key)): + continue + + if member_event.content["membership"] not in { + Membership.JOIN, + Membership.INVITE + }: + continue + + if ( + "kind" not in member_event.content + or member_event.content["kind"] != "guest" + ): + continue + + # We make the user choose to leave, rather than have the + # event-sender kick them. This is partially because we don't + # need to worry about power levels, and partially because guest + # users are a concept which doesn't hugely work over federation, + # and having homeservers have their own users leave keeps more + # of that decision-making and control local to the guest-having + # homeserver. + message_handler = self.hs.get_handlers().message_handler + yield message_handler.create_and_send_event( + { + "type": EventTypes.Member, + "state_key": member_event.state_key, + "content": { + "membership": Membership.LEAVE, + "kind": "guest" + }, + "room_id": member_event.room_id, + "sender": member_event.state_key + }, + ratelimit=False, + ) + except Exception as e: + logger.warn("Error kicking guest user: %s" % (e,)) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 872051b8b9..d1589334a5 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1097,8 +1097,6 @@ class FederationHandler(BaseHandler): context = yield self._prep_event( origin, event, state=state, - backfilled=backfilled, - current_state=current_state, auth_events=auth_events, ) @@ -1121,7 +1119,6 @@ class FederationHandler(BaseHandler): origin, ev_info["event"], state=ev_info.get("state"), - backfilled=backfilled, auth_events=ev_info.get("auth_events"), ) for ev_info in event_infos @@ -1208,8 +1205,7 @@ class FederationHandler(BaseHandler): defer.returnValue((event_stream_id, max_stream_id)) @defer.inlineCallbacks - def _prep_event(self, origin, event, state=None, backfilled=False, - current_state=None, auth_events=None): + def _prep_event(self, origin, event, state=None, auth_events=None): outlier = event.internal_metadata.is_outlier() context = yield self.state_handler.compute_event_context( @@ -1242,6 +1238,10 @@ class FederationHandler(BaseHandler): context.rejected = RejectedReason.AUTH_ERROR + if event.type == EventTypes.GuestAccess: + full_context = yield self.store.get_current_state(room_id=event.room_id) + yield self.maybe_kick_guest_users(event, full_context) + defer.returnValue(context) @defer.inlineCallbacks diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 654ecd2b37..7d31ff8d46 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -167,7 +167,7 @@ class MessageHandler(BaseHandler): @defer.inlineCallbacks def create_and_send_event(self, event_dict, ratelimit=True, - token_id=None, txn_id=None): + token_id=None, txn_id=None, is_guest=False): """ Given a dict from a client, create and handle a new event. Creates an FrozenEvent object, filling out auth_events, prev_events, @@ -213,7 +213,7 @@ class MessageHandler(BaseHandler): if event.type == EventTypes.Member: member_handler = self.hs.get_handlers().room_member_handler - yield member_handler.change_membership(event, context) + yield member_handler.change_membership(event, context, is_guest=is_guest) else: yield self.handle_new_client_event( event=event, diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 0b780cd528..aca65096fc 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -950,7 +950,8 @@ class PresenceHandler(BaseHandler): ) while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS: self._remote_offline_serials.pop() # remove the oldest - del self._user_cachemap[user] + if user in self._user_cachemap: + del self._user_cachemap[user] else: # Remove the user from remote_offline_serials now that they're # no longer offline diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 834972a580..7d18218cd9 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -369,7 +369,7 @@ class RoomMemberHandler(BaseHandler): remotedomains.add(member.domain) @defer.inlineCallbacks - def change_membership(self, event, context, do_auth=True): + def change_membership(self, event, context, do_auth=True, is_guest=False): """ Change the membership status of a user in a room. Args: @@ -390,6 +390,20 @@ class RoomMemberHandler(BaseHandler): # if this HS is not currently in the room, i.e. we have to do the # invite/join dance. if event.membership == Membership.JOIN: + if is_guest: + guest_access = context.current_state.get( + (EventTypes.GuestAccess, ""), + None + ) + is_guest_access_allowed = ( + guest_access + and guest_access.content + and "guest_access" in guest_access.content + and guest_access.content["guest_access"] == "can_join" + ) + if not is_guest_access_allowed: + raise AuthError(403, "Guest access not allowed") + yield self._do_join(event, context, do_auth=do_auth) else: if event.membership == Membership.LEAVE: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index e88a1ae290..03ac073926 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -175,7 +175,7 @@ class RoomSendEventRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_POST(self, request, room_id, event_type, txn_id=None): - user, token_id, _ = yield self.auth.get_user_by_req(request) + user, token_id, _ = yield self.auth.get_user_by_req(request, allow_guest=True) content = _parse_json(request) msg_handler = self.handlers.message_handler @@ -220,7 +220,10 @@ class JoinRoomAliasServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_POST(self, request, room_identifier, txn_id=None): - user, token_id, _ = yield self.auth.get_user_by_req(request) + user, token_id, is_guest = yield self.auth.get_user_by_req( + request, + allow_guest=True + ) # the identifier could be a room alias or a room id. Try one then the # other if it fails to parse, without swallowing other valid @@ -242,16 +245,20 @@ class JoinRoomAliasServlet(ClientV1RestServlet): defer.returnValue((200, ret_dict)) else: # room id msg_handler = self.handlers.message_handler + content = {"membership": Membership.JOIN} + if is_guest: + content["kind"] = "guest" yield msg_handler.create_and_send_event( { "type": EventTypes.Member, - "content": {"membership": Membership.JOIN}, + "content": content, "room_id": identifier.to_string(), "sender": user.to_string(), "state_key": user.to_string(), }, token_id=token_id, txn_id=txn_id, + is_guest=is_guest, ) defer.returnValue((200, {"room_id": identifier.to_string()})) -- cgit 1.4.1 From e93d550b79e7e485bb2866ef956fca2379cefac7 Mon Sep 17 00:00:00 2001 From: Daniel Wagner-Hall Date: Wed, 11 Nov 2015 17:49:44 +0000 Subject: Allow guests to access room context API --- synapse/handlers/room.py | 12 +++++++++--- synapse/rest/client/v1/room.py | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 7d18218cd9..0266926fc7 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -765,7 +765,7 @@ class RoomListHandler(BaseHandler): class RoomContextHandler(BaseHandler): @defer.inlineCallbacks - def get_event_context(self, user, room_id, event_id, limit): + def get_event_context(self, user, room_id, event_id, limit, is_guest): """Retrieves events, pagination tokens and state around a given event in a room. @@ -789,11 +789,17 @@ class RoomContextHandler(BaseHandler): ) results["events_before"] = yield self._filter_events_for_client( - user.to_string(), results["events_before"] + user.to_string(), + results["events_before"], + is_guest=is_guest, + require_all_visible_for_guests=False ) results["events_after"] = yield self._filter_events_for_client( - user.to_string(), results["events_after"] + user.to_string(), + results["events_after"], + is_guest=is_guest, + require_all_visible_for_guests=False ) if results["events_after"]: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 03ac073926..c583025e30 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -417,12 +417,12 @@ class RoomEventContext(ClientV1RestServlet): @defer.inlineCallbacks def on_GET(self, request, room_id, event_id): - user, _ = yield self.auth.get_user_by_req(request) + user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True) limit = int(request.args.get("limit", [10])[0]) results = yield self.handlers.room_context_handler.get_event_context( - user, room_id, event_id, limit, + user, room_id, event_id, limit, is_guest ) time_now = self.clock.time_msec() -- cgit 1.4.1 From c0b3554401b821002acfc3989e57d6ac59671aa9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 12 Nov 2015 16:19:55 +0000 Subject: Fix missing profile data in federation joins There was a regression where we stopped including profile data in initial joins for rooms joined over federation. --- synapse/federation/federation_client.py | 5 ++++- synapse/handlers/federation.py | 11 +++++++---- synapse/handlers/room.py | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) (limited to 'synapse/handlers/room.py') diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index c0c0b693b8..d4f586fae7 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -357,7 +357,8 @@ class FederationClient(FederationBase): defer.returnValue(signed_auth) @defer.inlineCallbacks - def make_membership_event(self, destinations, room_id, user_id, membership): + def make_membership_event(self, destinations, room_id, user_id, membership, + content={},): """ Creates an m.room.member event, with context, without participating in the room. @@ -398,6 +399,8 @@ class FederationClient(FederationBase): logger.debug("Got response to make_%s: %s", membership, pdu_dict) + pdu_dict["content"].update(content) + defer.returnValue( (destination, self.event_from_pdu_json(pdu_dict)) ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d1589334a5..c1bce07e31 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -564,7 +564,7 @@ class FederationHandler(BaseHandler): @log_function @defer.inlineCallbacks - def do_invite_join(self, target_hosts, room_id, joinee): + def do_invite_join(self, target_hosts, room_id, joinee, content): """ Attempts to join the `joinee` to the room `room_id` via the server `target_host`. @@ -584,7 +584,8 @@ class FederationHandler(BaseHandler): target_hosts, room_id, joinee, - "join" + "join", + content, ) self.room_queues[room_id] = [] @@ -840,12 +841,14 @@ class FederationHandler(BaseHandler): defer.returnValue(None) @defer.inlineCallbacks - def _make_and_verify_event(self, target_hosts, room_id, user_id, membership): + def _make_and_verify_event(self, target_hosts, room_id, user_id, membership, + content={},): origin, pdu = yield self.replication_layer.make_membership_event( target_hosts, room_id, user_id, - membership + membership, + content, ) logger.debug("Got response to make_%s: %s", membership, pdu) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 0266926fc7..3f04752581 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -504,7 +504,8 @@ class RoomMemberHandler(BaseHandler): yield handler.do_invite_join( room_hosts, room_id, - event.user_id + event.user_id, + event.content, ) else: logger.debug("Doing normal join") -- cgit 1.4.1