From 2a2b2ef8343fdc00a57df396dc76ce2b155b445d Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 21 Dec 2015 20:21:52 +0000 Subject: [PATCH 1/7] Remove bogus comment about branch coverage --- jenkins.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index bb0ae361a..e2bb706c7 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -6,7 +6,6 @@ export PYTHONDONTWRITEBYTECODE=yep export TRIAL_FLAGS="--reporter=subunit" export TOXSUFFIX="| subunit-1to2 | subunit2junitxml --no-passthrough --output-to=results.xml" # Write coverage reports to a separate file for each process -# Include branch coverage export COVERAGE_OPTS="-p" export DUMP_COVERAGE_COMMAND="coverage help" From 489a4cd1cf43f930e5d8fe27a08e02f975944c2d Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 21 Dec 2015 21:10:41 +0000 Subject: [PATCH 2/7] Add top level filtering by room id --- synapse/api/filtering.py | 63 +++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index bc03d6c28..35faa5374 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -62,10 +62,24 @@ class Filtering(object): self._check_definition(user_filter_json[key]) if "room" in user_filter_json: + self._check_definition_room_lists(user_filter_json["room"]) for key in room_level_definitions: if key in user_filter_json["room"]: self._check_definition(user_filter_json["room"][key]) + def _check_definition_room_lists(self, definition): + """Check that "rooms" and "not_rooms" are lists of room ids if they + are present + """ + # check rooms are valid room IDs + room_id_keys = ["rooms", "not_rooms"] + for key in room_id_keys: + if key in definition: + if type(definition[key]) != list: + raise SynapseError(400, "Expected %s to be a list." % key) + for room_id in definition[key]: + RoomID.from_string(room_id) + def _check_definition(self, definition): """Check if the provided definition is valid. @@ -85,14 +99,7 @@ class Filtering(object): 400, "Expected JSON object, not %s" % (definition,) ) - # check rooms are valid room IDs - room_id_keys = ["rooms", "not_rooms"] - for key in room_id_keys: - if key in definition: - if type(definition[key]) != list: - raise SynapseError(400, "Expected %s to be a list." % key) - for room_id in definition[key]: - RoomID.from_string(room_id) + self._check_definition_room_lists(definition) # check senders are valid user IDs user_id_keys = ["senders", "not_senders"] @@ -119,29 +126,19 @@ class FilterCollection(object): def __init__(self, filter_json): self.filter_json = filter_json - self.room_timeline_filter = Filter( - self.filter_json.get("room", {}).get("timeline", {}) - ) + room_filter_json = self.filter_json.get("room", {}) - self.room_state_filter = Filter( - self.filter_json.get("room", {}).get("state", {}) - ) + self.room_filter = Filter({ + k: v for k, v in room_filter_json.items() + if k in ("rooms", "not_rooms") + }) - self.room_ephemeral_filter = Filter( - self.filter_json.get("room", {}).get("ephemeral", {}) - ) - - self.room_account_data = Filter( - self.filter_json.get("room", {}).get("account_data", {}) - ) - - self.presence_filter = Filter( - self.filter_json.get("presence", {}) - ) - - self.account_data = Filter( - self.filter_json.get("account_data", {}) - ) + self.room_timeline_filter = Filter(room_filter_json.get("timeline", {})) + self.room_state_filter = Filter(room_filter_json.get("state", {})) + self.room_ephemeral_filter = Filter(room_filter_json.get("ephemeral", {})) + self.room_account_data = Filter(room_filter_json.get("account_data", {})) + self.presence_filter = Filter(self.filter_json.get("presence", {})) + self.account_data = Filter(self.filter_json.get("account_data", {})) self.include_leave = self.filter_json.get("room", {}).get( "include_leave", False @@ -163,16 +160,16 @@ class FilterCollection(object): return self.account_data.filter(events) def filter_room_state(self, events): - return self.room_state_filter.filter(events) + return self.room_state_filter.filter(self.room_filter.filter(events)) def filter_room_timeline(self, events): - return self.room_timeline_filter.filter(events) + return self.room_timeline_filter.filter(self.room_filter.filter(events)) def filter_room_ephemeral(self, events): - return self.room_ephemeral_filter.filter(events) + return self.room_ephemeral_filter.filter(self.room_filter.filter(events)) def filter_room_account_data(self, events): - return self.room_account_data.filter(events) + return self.room_account_data.filter(self.room_filter.filter(events)) class Filter(object): From 45a9e0ae0c9a4c55d4648802fefe96cc2933304f Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 22 Dec 2015 10:25:46 +0000 Subject: [PATCH 3/7] Allow guest access if the user provides a list of rooms in the filter --- synapse/api/filtering.py | 12 ++++++++++++ synapse/handlers/sync.py | 1 + synapse/rest/client/v2_alpha/sync.py | 10 +++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 35faa5374..8c8c7b642 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -144,6 +144,9 @@ class FilterCollection(object): "include_leave", False ) + def list_rooms(self): + return self.room_filter.list_rooms() + def timeline_limit(self): return self.room_timeline_filter.limit() @@ -176,6 +179,15 @@ class Filter(object): def __init__(self, filter_json): self.filter_json = filter_json + def list_rooms(self): + """The list of room_id strings this filter restricts the output to + or None if the this filter doesn't list the room ids. + """ + if "rooms" in self.filter_json: + return list(set(self.filter_json["rooms"])) + else: + return None + def check(self, event): """Checks whether the filter matches the given event. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 7088c20cb..75ef74232 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -29,6 +29,7 @@ logger = logging.getLogger(__name__) SyncConfig = collections.namedtuple("SyncConfig", [ "user", + "is_guest", "filter", ]) diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 697df03dd..35a70ffad 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -85,7 +85,9 @@ class SyncRestServlet(RestServlet): @defer.inlineCallbacks def on_GET(self, request): - 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 + ) timeout = parse_integer(request, "timeout", default=0) since = parse_string(request, "since") @@ -118,8 +120,14 @@ class SyncRestServlet(RestServlet): except: filter = FilterCollection({}) + if is_guest and filter.list_rooms() is None: + raise SynapseError( + 400, "Guest users must provide a list of rooms in the filter" + ) + sync_config = SyncConfig( user=user, + is_guest=is_guest, filter=filter, ) From c3fff251a99bc512d4f9ec362152bd532b4808ef Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 22 Dec 2015 11:21:03 +0000 Subject: [PATCH 4/7] Allow guest access to /sync --- synapse/handlers/sync.py | 144 ++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 46 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 75ef74232..9c8ea2bbb 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -15,8 +15,8 @@ from ._base import BaseHandler -from synapse.streams.config import PaginationConfig from synapse.api.constants import Membership, EventTypes +from synapse.api.errors import AuthError from synapse.util import unwrapFirstError from twisted.internet import defer @@ -118,6 +118,8 @@ class SyncResult(collections.namedtuple("SyncResult", [ self.presence or self.joined or self.invited ) +GuestRoom = collections.namedtuple("GuestRoom", ("room_id", "membership")) + class SyncHandler(BaseHandler): @@ -136,6 +138,12 @@ class SyncHandler(BaseHandler): A Deferred SyncResult. """ + if sync_config.is_guest: + for room_id in sync_config.filter.list_rooms(): + world_readable = yield self._is_world_readable(room_id) + if not world_readable: + raise AuthError(403, "Guest access not allowed") + if timeout == 0 or since_token is None or full_state: # we are going to return immediately, so don't bother calling # notifier.wait_for_events. @@ -152,6 +160,17 @@ class SyncHandler(BaseHandler): ) defer.returnValue(result) + @defer.inlineCallbacks + def _is_world_readable(self, room_id): + state = yield self.hs.get_state_handler().get_current_state( + room_id, + EventTypes.RoomHistoryVisibility + ) + if state and "history_visibility" in state.content: + defer.returnValue(state.content["history_visibility"] == "world_readable") + else: + defer.returnValue(False) + def current_sync_for_user(self, sync_config, since_token=None, full_state=False): """Get the sync for client needed to match what the server has now. @@ -175,37 +194,54 @@ class SyncHandler(BaseHandler): """ now_token = yield self.event_sources.get_current_token() - now_token, ephemeral_by_room = yield self.ephemeral_by_room( - sync_config, now_token - ) + if sync_config.is_guest: + room_list = [] + for room_id in sync_config.filter.list_rooms(): + room_list.append(GuestRoom(room_id, Membership.JOIN)) - presence_stream = self.event_sources.sources["presence"] - # TODO (mjark): This looks wrong, shouldn't we be getting the presence - # UP to the present rather than after the present? - pagination_config = PaginationConfig(from_token=now_token) - presence, _ = yield presence_stream.get_pagination_rows( - user=sync_config.user, - pagination_config=pagination_config.get_source_config("presence"), - key=None - ) + account_data = {} + account_data_by_room = {} + tags_by_room = {} - membership_list = (Membership.INVITE, Membership.JOIN) - if sync_config.filter.include_leave: - membership_list += (Membership.LEAVE, Membership.BAN) + # TODO: Hook up read receipts + ephemeral_by_room = {} - room_list = yield self.store.get_rooms_for_user_where_membership_is( - user_id=sync_config.user.to_string(), - membership_list=membership_list - ) + else: + now_token, ephemeral_by_room = yield self.ephemeral_by_room( + sync_config, now_token + ) - account_data, account_data_by_room = ( - yield self.store.get_account_data_for_user( + membership_list = (Membership.INVITE, Membership.JOIN) + if sync_config.filter.include_leave: + membership_list += (Membership.LEAVE, Membership.BAN) + + room_list = yield self.store.get_rooms_for_user_where_membership_is( + user_id=sync_config.user.to_string(), + membership_list=membership_list + ) + + account_data, account_data_by_room = ( + yield self.store.get_account_data_for_user( + sync_config.user.to_string() + ) + ) + + tags_by_room = yield self.store.get_tags_for_user( sync_config.user.to_string() ) - ) - tags_by_room = yield self.store.get_tags_for_user( - sync_config.user.to_string() + presence_stream = self.event_sources.sources["presence"] + + joined_room_ids = [ + room.room_id for room in room_list + if room.membership == Membership.JOIN + ] + + presence, _ = yield presence_stream.get_new_events( + from_key=0, + user=sync_config.user, + room_ids=joined_room_ids, + is_guest=sync_config.is_guest, ) joined = [] @@ -411,8 +447,36 @@ 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] + if sync_config.is_guest: + room_ids = sync_config.filter.list_rooms() + + ephemeral_by_room = {} + + tags_by_room = {} + account_data = {} + account_data_by_room = {} + + else: + rooms = yield self.store.get_rooms_for_user( + sync_config.user.to_string() + ) + room_ids = [room.room_id for room in rooms] + + now_token, ephemeral_by_room = yield self.ephemeral_by_room( + sync_config, now_token, since_token + ) + + tags_by_room = yield self.store.get_updated_tags( + sync_config.user.to_string(), + since_token.account_data_key, + ) + + account_data, account_data_by_room = ( + yield self.store.get_updated_account_data_for_user( + sync_config.user.to_string(), + since_token.account_data_key, + ) + ) presence_source = self.event_sources.sources["presence"] presence, presence_key = yield presence_source.get_new_events( @@ -420,15 +484,10 @@ class SyncHandler(BaseHandler): 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, + is_guest=sync_config.is_guest, ) now_token = now_token.copy_and_replace("presence_key", presence_key) - now_token, ephemeral_by_room = yield self.ephemeral_by_room( - sync_config, now_token, since_token - ) - rm_handler = self.hs.get_handlers().room_member_handler app_service = yield self.store.get_app_service_by_user_id( sync_config.user.to_string() @@ -448,18 +507,8 @@ class SyncHandler(BaseHandler): from_key=since_token.room_key, to_key=now_token.room_key, limit=timeline_limit + 1, - ) - - tags_by_room = yield self.store.get_updated_tags( - sync_config.user.to_string(), - since_token.account_data_key, - ) - - account_data, account_data_by_room = ( - yield self.store.get_updated_account_data_for_user( - sync_config.user.to_string(), - since_token.account_data_key, - ) + room_ids=room_ids if sync_config.is_guest else (), + is_guest=sync_config.is_guest, ) joined = [] @@ -591,7 +640,10 @@ class SyncHandler(BaseHandler): end_key = "s" + room_key.split('-')[-1] loaded_recents = sync_config.filter.filter_room_timeline(events) loaded_recents = yield self._filter_events_for_client( - sync_config.user.to_string(), loaded_recents, + sync_config.user.to_string(), + loaded_recents, + is_guest=sync_config.is_guest, + require_all_visible_for_guests=False ) loaded_recents.extend(recents) recents = loaded_recents From cdd04f70556c4f098b6975d07e2685b88aac8bf9 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 22 Dec 2015 11:59:55 +0000 Subject: [PATCH 5/7] Hook up read receipts and typing notifications for guest access --- synapse/handlers/sync.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 9c8ea2bbb..4753166a0 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -203,14 +203,7 @@ class SyncHandler(BaseHandler): account_data_by_room = {} tags_by_room = {} - # TODO: Hook up read receipts - ephemeral_by_room = {} - else: - now_token, ephemeral_by_room = yield self.ephemeral_by_room( - sync_config, now_token - ) - membership_list = (Membership.INVITE, Membership.JOIN) if sync_config.filter.include_leave: membership_list += (Membership.LEAVE, Membership.BAN) @@ -244,6 +237,10 @@ class SyncHandler(BaseHandler): is_guest=sync_config.is_guest, ) + now_token, ephemeral_by_room = yield self.ephemeral_by_room( + sync_config, now_token, joined_room_ids + ) + joined = [] invited = [] archived = [] @@ -352,11 +349,13 @@ class SyncHandler(BaseHandler): return account_data_events @defer.inlineCallbacks - def ephemeral_by_room(self, sync_config, now_token, since_token=None): + def ephemeral_by_room(self, sync_config, now_token, room_ids, + since_token=None): """Get the ephemeral events for each room the user is in Args: sync_config (SyncConfig): The flags, filters and user for the sync. now_token (StreamToken): Where the server is currently up to. + room_ids (list): List of room id strings to get data for. since_token (StreamToken): Where the server was when the client last synced. Returns: @@ -367,9 +366,6 @@ 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( user=sync_config.user, @@ -450,8 +446,6 @@ class SyncHandler(BaseHandler): if sync_config.is_guest: room_ids = sync_config.filter.list_rooms() - ephemeral_by_room = {} - tags_by_room = {} account_data = {} account_data_by_room = {} @@ -478,6 +472,10 @@ class SyncHandler(BaseHandler): ) ) + now_token, ephemeral_by_room = yield self.ephemeral_by_room( + sync_config, now_token, room_ids, since_token + ) + presence_source = self.event_sources.sources["presence"] presence, presence_key = yield presence_source.get_new_events( user=sync_config.user, From 251aafcccad74aef96def0adbf5d9387e1d19199 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 22 Dec 2015 14:03:24 +0000 Subject: [PATCH 6/7] Use a list comprehension --- synapse/handlers/sync.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 4753166a0..38c185cd5 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -195,9 +195,10 @@ class SyncHandler(BaseHandler): now_token = yield self.event_sources.get_current_token() if sync_config.is_guest: - room_list = [] - for room_id in sync_config.filter.list_rooms(): - room_list.append(GuestRoom(room_id, Membership.JOIN)) + room_list = [ + GuestRoom(room_id, Membership.JOIN) + for room_id in sync_config.filter.list_rooms() + ] account_data = {} account_data_by_room = {} From 0ee01383252cafd30ed92e3b655847d07cacee3a Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 22 Dec 2015 15:49:32 +0000 Subject: [PATCH 7/7] Include the list of bad room ids in the error --- synapse/api/errors.py | 16 ++++++++++++++++ synapse/handlers/sync.py | 10 ++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index d4037b3d5..8bc7b9e6d 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -120,6 +120,22 @@ class AuthError(SynapseError): super(AuthError, self).__init__(*args, **kwargs) +class GuestAccessError(AuthError): + """An error raised when a there is a problem with a guest user accessing + a room""" + + def __init__(self, rooms, *args, **kwargs): + self.rooms = rooms + super(GuestAccessError, self).__init__(*args, **kwargs) + + def error_dict(self): + return cs_error( + self.msg, + self.errcode, + rooms=self.rooms, + ) + + class EventSizeError(SynapseError): """An error raised when an event is too big.""" diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 38c185cd5..feea407ea 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -16,7 +16,7 @@ from ._base import BaseHandler from synapse.api.constants import Membership, EventTypes -from synapse.api.errors import AuthError +from synapse.api.errors import GuestAccessError from synapse.util import unwrapFirstError from twisted.internet import defer @@ -139,10 +139,16 @@ class SyncHandler(BaseHandler): """ if sync_config.is_guest: + bad_rooms = [] for room_id in sync_config.filter.list_rooms(): world_readable = yield self._is_world_readable(room_id) if not world_readable: - raise AuthError(403, "Guest access not allowed") + bad_rooms.append(room_id) + + if bad_rooms: + raise GuestAccessError( + bad_rooms, 403, "Guest access not allowed" + ) if timeout == 0 or since_token is None or full_state: # we are going to return immediately, so don't bother calling