This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch db/8607d in repository https://gitbox.apache.org/repos/asf/allura.git
commit 03a442ee7680abb5dd3659c722a0f94624829262 Author: Dave Brondsema <[email protected]> AuthorDate: Fri Jun 12 17:42:14 2026 -0400 [#8607] perm checks on ticket bulk edit & move --- ForgeTracker/forgetracker/model/ticket.py | 17 ++++++++++++++++- .../forgetracker/tests/functional/test_root.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py index 17f143fe5..0434ba2b0 100644 --- a/ForgeTracker/forgetracker/model/ticket.py +++ b/ForgeTracker/forgetracker/model/ticket.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations import logging import six.moves.urllib.request import six.moves.urllib.parse @@ -519,7 +520,11 @@ def changes_iter(user): count, 's' if count != 1 else '', app) Notification.post_user(c.user, None, 'flash', text=text) - def filtered_by_subscription(self, tickets, project_id=None, app_config_id=None): + def filtered_by_subscription( + self, tickets: dict[ObjectId, Ticket], + project_id: ObjectId | None = None, + app_config_id: ObjectId | None = None, + ) -> dict[ObjectId, set[ObjectId]]: # user_id -> set of ticket_ids p_id = project_id if project_id else c.project._id ac_id = app_config_id if app_config_id else self.app_config_id ticket_ids = list(tickets.keys()) @@ -537,6 +542,16 @@ def filtered_by_subscription(self, tickets, project_id=None, app_config_id=None) elif subscription.artifact_index_id in list(tickets_index_id.keys()): user = filtered.setdefault(subscription.user_id, set()) user.add(tickets_index_id[subscription.artifact_index_id]) + # Check for subscribers who can't read them (e.g. private tickets reached via a tool-wide subscription) + users_by_id = {u._id: u for u in User.query.find({'_id': {'$in': list(filtered.keys())}})} + for user_id, t_ids in list(filtered.items()): + user = users_by_id.get(user_id) + readable = {t_id for t_id in t_ids + if user and security.has_access(tickets[t_id], 'read', user)} + if readable: + filtered[user_id] = readable + else: + del filtered[user_id] return filtered def append_new_labels(self, old_labels, new_labels): diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py index 528a81a77..778e2de83 100644 --- a/ForgeTracker/forgetracker/tests/functional/test_root.py +++ b/ForgeTracker/forgetracker/tests/functional/test_root.py @@ -1989,6 +1989,27 @@ def test_filtered_by_subscription(self): assert filtered_changes[users[1]._id] == set(ticket_ids[:-1]) assert filtered_changes[admin._id] == set(ticket_ids[:-1]) + def test_filtered_by_subscription_respects_private_read_access(self): + self.new_ticket(summary='public ticket', status='open') + self.new_ticket(summary='private ticket', status='open') + public = tm.Ticket.query.get(summary='public ticket') + private = tm.Ticket.query.get(summary='private ticket') + private.private = True + user = M.User.by_username('test-user-0') + admin = M.User.by_username('test-admin') + # tool-wide subscription: would otherwise receive every ticket change + M.Mailbox.subscribe(user_id=user._id, artifact=None) + ThreadLocalODMSession.flush_all() + M.MonQTask.run_ready() + ThreadLocalODMSession.flush_all() + + changes = {public._id: public, private._id: private} + filtered = c.app.globals.filtered_by_subscription(changes) + # user can't read the private ticket, so it's excluded from their fanout + assert filtered[user._id] == {public._id} + # admin (developer/creator) can read both + assert filtered[admin._id] == {public._id, private._id} + def test_vote(self): r = self.new_ticket(summary='test vote').follow() assert r.html.find('div', {'id': 'vote'})
