You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2026/06/12 21:49:29 UTC
(allura) 05/05: [#8607] perm checks on ticket bulk edit & move
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 <da...@brondsema.net>
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'})