From 4572958c28471900f1ba91114e05e3246fbf652f Mon Sep 17 00:00:00 2001 From: Adam Coldrick Date: Wed, 24 Aug 2016 12:57:06 +0000 Subject: [PATCH] Hide timeline events and comments of private stories This commit fixes a bug wherein timeline events of private stories can be obtained by users without the correct permissions. Change-Id: Ifb7ef03938140930abbdb21a9bcf7e98ae39aaf6 --- storyboard/api/v1/search/sqlalchemy_impl.py | 7 ++- storyboard/api/v1/timeline.py | 45 ++++++++++---- storyboard/db/api/base.py | 2 +- storyboard/db/api/timeline_events.py | 66 +++++++++++++++++---- storyboard/db/models.py | 1 + 5 files changed, 94 insertions(+), 27 deletions(-) diff --git a/storyboard/api/v1/search/sqlalchemy_impl.py b/storyboard/api/v1/search/sqlalchemy_impl.py index b9558182..ed3d344a 100644 --- a/storyboard/api/v1/search/sqlalchemy_impl.py +++ b/storyboard/api/v1/search/sqlalchemy_impl.py @@ -91,10 +91,13 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine): return query.all() - def comments_query(self, q, marker=None, offset=None, - limit=None, **kwargs): + def comments_query(self, q, marker=None, offset=None, limit=None, + current_user=None, **kwargs): session = api_base.get_session() query = api_base.model_query(models.Comment, session) + query = query.outerjoin(models.Story) + query = api_base.filter_private_stories(query, current_user) + query = self._build_fulltext_search(models.Comment, query, q) query = self._apply_pagination( models.Comment, query, marker, offset, limit) diff --git a/storyboard/api/v1/timeline.py b/storyboard/api/v1/timeline.py index 1d279a6a..5fd39d33 100644 --- a/storyboard/api/v1/timeline.py +++ b/storyboard/api/v1/timeline.py @@ -29,6 +29,7 @@ from storyboard.common import decorators from storyboard.common import event_types from storyboard.common import exception as exc from storyboard.db.api import comments as comments_api +from storyboard.db.api import stories as stories_api from storyboard.db.api import timeline_events as events_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -57,7 +58,8 @@ class TimeLineEventsController(rest.RestController): :param event_id: An ID of the event. """ - event = events_api.event_get(event_id) + event = events_api.event_get(event_id, + current_user=request.current_user_id) if event: wsme_event = wmodels.TimeLineEvent.from_db_model(event) @@ -87,6 +89,8 @@ class TimeLineEventsController(rest.RestController): :param sort_dir: Sort direction for results (asc, desc). """ + current_user = request.current_user_id + # Boundary check on limit. if limit is not None: limit = max(0, limit) @@ -106,14 +110,16 @@ class TimeLineEventsController(rest.RestController): marker_event = events_api.event_get(marker) event_count = events_api.events_get_count(story_id=story_id, - event_type=event_type) + event_type=event_type, + current_user=current_user) events = events_api.events_get_all(story_id=story_id, event_type=event_type, marker=marker_event, offset=offset, limit=limit, sort_field=sort_field, - sort_dir=sort_dir) + sort_dir=sort_dir, + current_user=current_user) # Apply the query response headers. if limit: @@ -141,6 +147,15 @@ class CommentsHistoryController(rest.RestController): :param comment_id: The ID of the comment to inspect history of. """ comment = comments_api.comment_get(comment_id) + if comment is None: + raise exc.NotFound(_("Comment %s not found") % comment_id) + + # Check that the user can actually see the relevant story + story = stories_api.story_get_simple( + comment.event[0].story_id, current_user=request.current_user_id) + if story is None: + raise exc.NotFound(_("Comment %s not found") % comment_id) + return [wmodels.Comment.from_db_model(old_comment) for old_comment in comment.history] @@ -166,14 +181,18 @@ class CommentsController(rest.RestController): comments have their own unique ids. :param comment_id: An ID of the comment. """ - comment = comments_api.comment_get(comment_id) - - if comment: - return wmodels.Comment.from_db_model(comment) - else: + if comment is None: raise exc.NotFound(_("Comment %s not found") % comment_id) + # Check that the user can actually see the relevant story + story = stories_api.story_get_simple( + comment.event[0].story_id, current_user=request.current_user_id) + if story is None: + raise exc.NotFound(_("Comment %s not found") % comment_id) + + return wmodels.Comment.from_db_model(comment) + @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Comment], int, int, int, wtypes.text, @@ -192,6 +211,7 @@ class CommentsController(rest.RestController): :param sort_field: The name of the field to sort on. :param sort_dir: Sort direction for results (asc, desc). """ + current_user = request.current_user_id # Boundary check on limit. if limit is not None: @@ -202,20 +222,23 @@ class CommentsController(rest.RestController): if marker: event_query = \ events_api.events_get_all(comment_id=marker, - event_type=event_types.USER_COMMENT) + event_type=event_types.USER_COMMENT, + current_user=current_user) if len(event_query) > 0: marker_event = event_query[0] events_count = events_api.events_get_count( story_id=story_id, - event_type=event_types.USER_COMMENT) + event_type=event_types.USER_COMMENT, + current_user=current_user) events = events_api.events_get_all(story_id=story_id, marker=marker_event, limit=limit, event_type=event_types.USER_COMMENT, sort_field=sort_field, - sort_dir=sort_dir) + sort_dir=sort_dir, + current_user=current_user) comments = [comments_api.comment_get(event.comment_id) for event in events] diff --git a/storyboard/db/api/base.py b/storyboard/db/api/base.py index 4aae56b0..202c79a0 100644 --- a/storyboard/db/api/base.py +++ b/storyboard/db/api/base.py @@ -472,7 +472,7 @@ def filter_private_worklists(query, current_user, hide_lanes=True): or_( and_( users.id == current_user, - boards.private == true() + models.Worklist.private == true() ), models.Worklist.private == false(), models.Worklist.id.is_(None) diff --git a/storyboard/db/api/timeline_events.py b/storyboard/db/api/timeline_events.py index f22990c8..c1569120 100644 --- a/storyboard/db/api/timeline_events.py +++ b/storyboard/db/api/timeline_events.py @@ -29,23 +29,63 @@ from storyboard.notifications.publisher import publish CONF = cfg.CONF -def event_get(event_id): - return api_base.entity_get(models.TimeLineEvent, event_id) +def event_get(event_id, session=None, current_user=None): + query = (api_base.model_query(models.TimeLineEvent, session) + .filter_by(id=event_id)) + query = query.outerjoin(models.Story) + query = api_base.filter_private_stories(query, current_user) + + query = query.outerjoin(models.Worklist) + query = api_base.filter_private_worklists( + query, current_user, hide_lanes=False) + + query = query.outerjoin(models.Board) + query = api_base.filter_private_boards(query, current_user) + + return query.first() -def events_get_all(marker=None, limit=None, sort_field=None, sort_dir=None, - **kwargs): - return api_base.entity_get_all(models.TimeLineEvent, - marker=marker, - limit=limit, - sort_field=sort_field, - sort_dir=sort_dir, - **kwargs) +def _events_build_query(current_user=None, **kwargs): + query = api_base.model_query(models.TimeLineEvent).distinct() + + query = api_base.apply_query_filters(query=query, + model=models.TimeLineEvent, + **kwargs) + + query = query.outerjoin(models.Story) + query = api_base.filter_private_stories(query, current_user) + + query = query.outerjoin(models.Worklist) + query = api_base.filter_private_worklists( + query, current_user, hide_lanes=False) + + query = query.outerjoin(models.Board) + query = api_base.filter_private_boards(query, current_user) + + return query -def events_get_count(**kwargs): - return api_base.entity_get_count(models.TimeLineEvent, - **kwargs) +def events_get_all(marker=None, offset=None, limit=None, sort_field=None, + sort_dir=None, current_user=None, **kwargs): + if sort_field is None: + sort_field = 'id' + if sort_dir is None: + sort_dir = 'asc' + + query = _events_build_query(current_user=current_user, **kwargs) + query = api_base.paginate_query(query=query, + model=models.TimeLineEvent, + marker=marker, + limit=limit, + offset=offset, + sort_key=sort_field, + sort_dir=sort_dir) + return query.all() + + +def events_get_count(current_user=None, **kwargs): + query = _events_build_query(current_user=current_user, **kwargs) + return query.count() def event_create(values): diff --git a/storyboard/db/models.py b/storyboard/db/models.py index b6ef191f..086f0f35 100644 --- a/storyboard/db/models.py +++ b/storyboard/db/models.py @@ -490,6 +490,7 @@ class TimeLineEvent(ModelBuilder, Base): worklist_id = Column(Integer, ForeignKey('worklists.id'), nullable=True) board_id = Column(Integer, ForeignKey('boards.id'), nullable=True) comment_id = Column(Integer, ForeignKey('comments.id'), nullable=True) + comment = relationship('Comment', backref='event') author_id = Column(Integer, ForeignKey('users.id'), nullable=True) event_type = Column(Enum(*event_types.ALL), nullable=False)