Opened 6 years ago
Last modified 4 years ago
#13282 new defect
Ticket timeline events can't be filtered using a permission policy
| Reported by: | Ryan J Ollos | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | 1.7.1 | 
| Component: | ticket system | Version: | |
| Severity: | normal | Keywords: | |
| Cc: | virginia.alcaide@… | Branch: | |
| Release Notes: | |||
| API Changes: | |||
| Internal Changes: | |||
Description
I'm trying to implement a solution for private ticket comments (th:#13776) and finding that the permission checking for timeline events is too coarse to implement a permissions policy that filters by resource realm comment and resource id cnum.
Attachments (2)
Change History (18)
comment:1 by , 6 years ago
comment:2 by , 6 years ago
#12719 created a comment resource and performed permission checks on that resource. Looking at this again, it appears we can use version for the permission check on the ticket resource in place of the comment resource. For example:
- comment_resource = \ - Resource('comment', cnum, parent=ticket.resource) - req.perm(comment_resource).require('TICKET_EDIT_COMMENT') + req.perm(ticket.resource(version=cnum)).require('TICKET_EDIT_COMMENT')
comment:3 by , 6 years ago
PrivateCommentsPlugin adds a checkbox to the comment form. It saves the private comment state to a table but needs the ticket changes to be applied first so that ticket['changetime'] can be saved in the private_comments table.
I've tried implementing this numerous ways, using IRequestFilter and ITicketActionController. I've tried using ITicketManipulator in combination with redirect listener, which I think probably works okay.
Another way to make this work is a combination of ITicketManipulator and ITicketChangeListener. The former grabs the value from the request and attaches is to the Ticket object. The latter saves the value to the database.
def validate_ticket(self, req, ticket): if 'private_comment' in req.args: ticket.private_comment = req.args['private_comment'] return [] def ticket_changed(self, ticket, comment, author, old_values): if hasattr(ticket, 'private_comment') and ticket.private_comment: self._insert_private_comment(ticket, ticket['changetime']) def ticket_comment_modified(self, ticket, cdate, author, comment, old_comment): if hasattr(ticket, 'private_comment') and ticket.private_comment: self._insert_private_comment(ticket, cdate) else: self._delete_private_comment(ticket, cdate)
I think this has been discussed before, the issues around change listeners not having access to the request. I need to do a deep dive on some of those tickets (#11148).
Another way to accomplish this would be a method added to ITicketManipulator:
- 
      
trac/ticket/api.py
diff --git a/trac/ticket/api.py b/trac/ticket/api.py index 28e3dfaf5..c9ce57512 100644
a b class ITicketManipulator(Interface): 181 181 :since: 1.3.2 182 182 """ 183 183 184 def apply_side_effects(req, ticket): 185 """Apply side effects after ticket has been changed. 186 187 This should not be used to make changes to the ticket, 188 rather to apply changes outside of the `Ticket` class. 189 190 :since: 1.4.2 191 """ 192 184 193 185 194 class IMilestoneChangeListener(Interface): 186 195 """Extension point interface for components that require notification  - 
      
trac/ticket/web_ui.py
diff --git a/trac/ticket/web_ui.py b/trac/ticket/web_ui.py index 86067a46e..724198f0d 100644
a b class TicketModule(Component): 636 636 req.perm(ticket.resource).require('TICKET_EDIT_COMMENT') 637 637 if self._validate_ticket(req, ticket): 638 638 ticket.modify_comment(change['date'], req.authname, comment) 639 self._apply_side_effects(req, ticket) 639 640 req.redirect(req.href.ticket(ticket.id) + 640 641 '#comment:%d' % cnum) 641 642 else: … … class TicketModule(Component): 1362 1363 controller.__class__.__name__) 1363 1364 controller.apply_action_side_effects(req, ticket, action) 1364 1365 1366 self._apply_side_effects(req, ticket) 1367 1365 1368 # Notify. Use author=req.authname rather than ticket['reporter'] 1366 1369 # because ticket may be created on behalf of another user (#11949). 1367 1370 event = TicketChangeEvent('created', ticket, ticket['time'], … … class TicketModule(Component): 1429 1432 controller.__class__.__name__) 1430 1433 controller.apply_action_side_effects(req, ticket, action) 1431 1434 1435 self._apply_side_effects(req, ticket) 1436 1432 1437 req.redirect(req.href.ticket(ticket.id) + fragment) 1433 1438 1434 1439 def get_ticket_changes(self, req, ticket, selected_action): … … class TicketModule(Component): 1480 1485 for key in field_changes: 1481 1486 ticket[key] = field_changes[key]['new'] 1482 1487 1488 def _apply_side_effects(self, req, ticket): 1489 for manipulator in self.ticket_manipulators: 1490 if hasattr(manipulator, 'apply_side_effects'): 1491 manipulator.apply_side_effects(req, ticket) 1492 1483 1493 def _query_link(self, req, name, value, text=None, class_=None): 1484 1494 """Return a link to /query with the appropriate name and value""" 1485 1495 from trac.ticket.query import QueryModule  
def apply_side_effects(self, req, ticket): if 'id' in req.args: # Only if ticket exists cdate = None if 'cnum_edit' in req.args: change = ticket.get_change(req.args.getint('cnum_edit')) cdate = change['date'] if 'private_comment' in req.args: self._insert_private_comment(ticket, cdate) else: self._delete_private_comment(ticket, cdate)
With this change, ITicketManipulator has methods that get called before (validate_ticket/validate_comment) and after (apply_side_effects, or apply_ticket_side_effects to avoid name clashes) ticket changes.
Thoughts?
comment:4 by , 6 years ago
I was thinking about adding an attribute check before each interface method is called. This way plugins can implement just the methods of the interface they intend to use and we don't have code like:
def ticket_created(self): pass
Example:
- 
      
trac/ticket/model.py
diff --git a/trac/ticket/model.py b/trac/ticket/model.py index f4b3961e8..6d7a7fcd9 100644
a b class Ticket(object): 320 320 self._old = {} 321 321 322 322 for listener in TicketSystem(self.env).change_listeners: 323 listener.ticket_created(self) 323 if hasattr(listener, 'ticket_created'): 324 listener.ticket_created(self) 324 325 325 326 return self.id  
comment:5 by , 6 years ago
| Summary: | Timeline events can't be filtered using a permission policy → Ticket timeline events can't be filtered using a permission policy | 
|---|
by , 6 years ago
| Attachment: | Screen Shot 2020-05-06 at 13.11.02.jpg added | 
|---|
by , 6 years ago
| Attachment: | Screen Shot 2020-05-06 at 13.11.43.jpg added | 
|---|
comment:6 by , 6 years ago
My intent here was just to fix-up a plugin, but now I'm looking at this as a case revealing some defects and shortcoming in the architecture. The plugin does some basic things:
- Adds a Private Comment checkbox to the comment and edit comment forms.
 - Creates a table 
private_commentsfor saving the resource id and version of private comments. - Defines 
TICKET_VIEW_PRIVATE_COMMENTviaIPermissionRequestor. - Implements a permission policy via 
IPermissionPolicythat checksTICKET_VIEW_PRIVATE_COMMENTand queriesprivate_comments. - Specializes 
EmailDistributorto filter recipients per the permissions policy. - Adds checkbox to the ticket form for setting a comment as private, shown below.
 
Summarizing findings:
- Numerous changes are needed to Trac so that ticket comment visibility can be restricted based on resource 
idandversion. - Ticket 
changetime/cnumare not known untilticket.save_changesis called (thecnumcontains the resourceversion).ITicketManipulatormethods are only called before changes are saved (thus my proposal to add a method that is called after saving changes).RequestFilter.post_process_requestisn't called for thePOSTrequest because there is a redirect. It's possible that a redirect listener could be used here, I haven't revisited that solution, but it would probably be complex. ITicketChangeListenermight otherwise be used to save the ticketidandversionin aprivate_commentstable, but there are two problems:- It doesn't have access to the 
reqobject. This can be overcome by taking the value from the request inITicketManipulator.validate_ticketorIRequestFilter.pre_process_requestand adding it to theTicketobject. ITicketChangeListener.ticket_comment_modifiedisn't called if a ticket comment edit is saved without changing the comment, so a comment edit that just changes the Private Comment checkbox state isn't saved.
- It doesn't have access to the 
 
comment:8 by , 6 years ago
comment:9 by , 5 years ago
Considering comment:9:ticket:12414, whether ticket version is the same cnum.
Even if there were attachments or comment edits (two ways to change the ticket without incrementing the cnum)?
On current 1.4-stable, it appears they are the same. Attachments are not "permanent" and neither attachment changes nor comment edits change the version.
For the standpoint of a permissions policy, it would probably be better if every change to a ticket incremented the version. Appending ?version=N would the give a snapshot of the ticket. Consider what actually happens:
- Create ticket: version=0
 - Append comment: version=1
 - Edit comment: version=1
 - Add attachment: version=1
 
When viewing a ticket version, for ?version=1 I'd expect to see state (1), and for ?version=3 I'd expect to see the state (3).
Thank you for raising that point, it's helping with investigating and my understanding everything.
comment:10 by , 5 years ago
| Milestone: | 1.4.2 → 1.4.3 | 
|---|
comment:11 by , 5 years ago
| Cc: | added | 
|---|
comment:12 by , 5 years ago
| Milestone: | 1.4.3 → 1.5.3 | 
|---|
comment:13 by , 5 years ago
| Milestone: | 1.5.3 → 1.7 | 
|---|
comment:15 by , 4 years ago
| Milestone: | 1.7.1 | 
|---|---|
| Owner: | removed | 
| Status: | assigned → new | 
comment:16 by , 4 years ago
| Milestone: | → 1.7.1 | 
|---|





  
For now, a simple change will probably be sufficient:
trac/ticket/web_ui.py
)