privateticketsplugin/0.10/0000775000175000017500000000000011533751452016424 5ustar franciscofranciscoprivateticketsplugin/0.10/privatetickets/0000775000175000017500000000000011533751452021465 5ustar franciscofranciscoprivateticketsplugin/0.10/privatetickets/report.py0000775000175000017500000000405710600623506023353 0ustar franciscofranciscofrom trac.core import * from trac.web.api import IRequestFilter from trac.ticket.report import ReportModule from api import PrivateTicketsSystem __all__ = ['PrivateTicketsReportFilter'] class PrivateTicketsReportFilter(Component): """Show only ticket the user is involved in in the reports.""" implements(IRequestFilter) # IRequestFilter methods def pre_process_request(self, req, handler): if isinstance(handler, ReportModule) and \ not req.perm.has_permission('TICKET_VIEW') and \ req.args.get('format') in ('tab', 'csv'): raise TracError('Access denied') return handler def post_process_request(self, req, template, content_type): if req.args.get('DO_PRIVATETICKETS_FILTER') == 'report': # Walk the HDF fn = PrivateTicketsSystem(self.env).check_ticket_access deleted = [] left = [] node = req.hdf.getObj('report.items') if node is None: return template, content_type node = node.child() while node: i = node.name() id = req.hdf['report.items.%s.ticket'%i] if not fn(req, id): deleted.append(i) else: left.append(i) node = node.next() # Delete the needed subtrees for n in deleted: req.hdf.removeTree('report.items.%s'%n) # Recalculate this req.hdf['report.numrows'] = len(left) # Move the remaining items into their normal places for src, dest in zip(left, xrange(len(left)+len(deleted))): if src == dest: continue req.hdf.getObj('report.items').copy(str(dest), req.hdf.getObj('report.items.%s'%src)) for n in xrange(len(left), len(left)+len(deleted)): req.hdf.removeTree('report.items.%s'%n) return template, content_type privateticketsplugin/0.10/privatetickets/view.py0000775000175000017500000000557610614177560023032 0ustar franciscofranciscofrom trac.core import * from trac.web.chrome import INavigationContributor from trac.ticket.web_ui import TicketModule from trac.ticket.query import QueryModule from trac.Search import SearchModule from trac.ticket.report import ReportModule from trac.attachment import AttachmentModule from trac.util.html import html from api import PrivateTicketsSystem __all__ = ['PrivateTicketsViewModule'] class PrivateTicketsViewModule(Component): """Allow users to see tickets they are involved in.""" implements(INavigationContributor) # INavigationContributor methods def get_active_navigation_item(self, req): return '' def get_navigation_items(self, req): # Don't allow this to be exposed if 'DO_PRIVATETICKETS_FILTER' in req.args.keys(): del req.args['DO_PRIVATETICKETS_FILTER'] # Various ways to allow access if not req.perm.has_permission('TICKET_VIEW') and \ (req.perm.has_permission('TICKET_VIEW_REPORTER') or \ req.perm.has_permission('TICKET_VIEW_OWNER') or \ req.perm.has_permission('TICKET_VIEW_CC') or \ req.perm.has_permission('TICKET_VIEW_REPORTER_GROUP') or \ req.perm.has_permission('TICKET_VIEW_OWNER_GROUP') or \ req.perm.has_permission('TICKET_VIEW_CC_GROUP')): if TicketModule(self.env).match_request(req): if PrivateTicketsSystem(self.env).check_ticket_access(req, req.args['id']): self._grant_view(req) elif AttachmentModule(self.env).match_request(req): if req.args['type'] == 'ticket' and PrivateTicketsSystem(self.env).check_ticket_access(req, req.args['path'].split('/')[0]): self._grant_view(req) elif QueryModule(self.env).match_request(req): req.args['DO_PRIVATETICKETS_FILTER'] = 'query' self._grant_view(req) # Further filtering in query.py elif SearchModule(self.env).match_request(req): if 'ticket' in req.args.keys(): req.args['pticket'] = req.args['ticket'] del req.args['ticket'] elif ReportModule(self.env).match_request(req): self._grant_view(req) # So they can see the query page link if req.args.get('id'): req.args['DO_PRIVATETICKETS_FILTER'] = 'report' # NOTE: Send this back here because the button would be hidden otherwise. if not self.env.is_component_enabled(ReportModule) or not req.perm.has_permission('REPORT_VIEW'): return [('mainnav', 'tickets', html.A('View Tickets', href=req.href.query()))] return [] # Internal methods def _grant_view(self, req): req.perm.perms['TICKET_VIEW'] = True req.hdf['trac.acl.TICKET_VIEW'] = 1 privateticketsplugin/0.10/privatetickets/query.py0000775000175000017500000001473110533401022023175 0ustar franciscofranciscofrom trac.core import * from trac.web.api import IRequestFilter from trac.ticket.query import QueryModule, Query from trac.mimeview.api import Mimeview, IContentConverter from trac.wiki import wiki_to_html from trac.util.datefmt import http_date from trac.util.text import CRLF from StringIO import StringIO from api import PrivateTicketsSystem __all__ = ['PrivateTicketsQueryFilter'] class PrivateTicketsQueryFilter(Component): """Remove entires from queries if this user shouldn't see them.""" implements(IRequestFilter) # IRequestFilter methods def pre_process_request(self, req, handler): if isinstance(handler, QueryModule) and req.args.get('format'): self.log.debug('PrivateTickets: Intercepting formatted query') return self # XXX: Hack due to IContentConverter being b0rked return handler def post_process_request(self, req, template, content_type): if req.args.get('DO_PRIVATETICKETS_FILTER') == 'query': # Extract the data results = [] node = req.hdf.getObj('query.results') if not node: return template, content_type node = node.child() while node: data = {} sub_node = node.child() while sub_node: data[sub_node.name()] = sub_node.value() sub_node = sub_node.next() results.append(data) node = node.next() self.log.debug('PrivateTickets: results = %r', results) # Nuke the old data req.hdf.removeTree('query.results') # Filter down the data fn = PrivateTicketsSystem(self.env).check_ticket_access new_results = [d for d in results if fn(req, d['id'])] self.log.debug('PrivateTickets: new_results = %r', new_results) # Reinsert the data req.hdf['query.results'] = new_results return template, content_type # Content conversion insanity def process_request(self, req): constraints = QueryModule(self.env)._get_constraints(req) if not constraints and not req.args.has_key('order'): # avoid displaying all tickets when the query module is invoked # with no parameters. Instead show only open tickets, possibly # associated with the user constraints = {'status': ('new', 'assigned', 'reopened')} if req.authname and req.authname != 'anonymous': constraints['owner'] = (req.authname,) else: email = req.session.get('email') name = req.session.get('name') if email or name: constraints['cc'] = ('~%s' % email or name,) query = Query(self.env, constraints, req.args.get('order'), req.args.has_key('desc'), req.args.get('group'), req.args.has_key('groupdesc'), req.args.has_key('verbose')) format = req.args.get('format') self.send_converted(req, 'trac.ticket.Query', query, format, 'query') def get_supported_conversions(self): yield ('csv', 'Comma-delimited Text', 'csv', 'trac.ticket.Query', 'text/csv', 9) def convert_content(self, req, mimetype, query, key): if key == 'rss': return self.export_rss(req, query) + ('rss',) elif key == 'csv': return self.export_csv(req, query, mimetype='text/csv') + ('csv',) elif key == 'tab': return self.export_csv(req, query, '\t', 'text/tab-separated-values') + ('tsv',) def send_converted(self, req, in_type, content, selector, filename='file'): # Stolen from Mimetype """Helper method for converting `content` and sending it directly. `selector` can be either a key or a MIME Type.""" from trac.web import RequestDone content, output_type, ext = self.convert_content(req, in_type, content, selector) req.send_response(200) req.send_header('Content-Type', output_type) req.send_header('Content-Disposition', 'filename=%s.%s' % (filename, ext)) req.end_headers() req.write(content) raise RequestDone # Hacked content converters def export_csv(self, req, query, sep=',', mimetype='text/plain'): self.log.debug('PrivateTicket: Running hacked CSV converter') content = StringIO() cols = query.get_columns() content.write(sep.join([col for col in cols]) + CRLF) fn = PrivateTicketsSystem(self.env).check_ticket_access results = query.execute(req, self.env.get_db_cnx()) for result in results: # Filter data if not fn(req, result['id']): continue content.write(sep.join([unicode(result[col]).replace(sep, '_') .replace('\n', ' ') .replace('\r', ' ') for col in cols]) + CRLF) return (content.getvalue(), '%s;charset=utf-8' % mimetype) def export_rss(self, req, query): query.verbose = True db = self.env.get_db_cnx() fn = PrivateTicketsSystem(self.env).check_ticket_access results = [r for r in query.execute(req, db) if fn(req, r['id'])] for result in results: result['href'] = req.abs_href.ticket(result['id']) if result['reporter'].find('@') == -1: result['reporter'] = '' if result['description']: # unicode() cancels out the Markup() returned by wiki_to_html descr = wiki_to_html(result['description'], self.env, req, db, absurls=True) result['description'] = unicode(descr) if result['time']: result['time'] = http_date(result['time']) req.hdf['query.results'] = results req.hdf['query.href'] = req.abs_href.query(group=query.group, groupdesc=query.groupdesc and 1 or None, verbose=query.verbose and 1 or None, **query.constraints) return (req.hdf.render('query_rss.cs'), 'application/rss+xml') privateticketsplugin/0.10/privatetickets/__init__.py0000775000175000017500000000000010522011526023553 0ustar franciscofranciscoprivateticketsplugin/0.10/privatetickets/search.py0000775000175000017500000000347310522011526023302 0ustar franciscofranciscofrom trac.core import * from trac.Search import ISearchSource from trac.ticket.api import TicketSystem from trac.web.api import IRequestFilter from api import PrivateTicketsSystem __all__ = ['PrivateTicketsSearchModule'] class PrivateTicketsSearchModule(Component): """Search restricted to tickets you are involved with.""" implements(ISearchSource, IRequestFilter) # ISearchSource methods def get_search_filters(self, req): if not req.perm.has_permission('TICKET_VIEW') and \ ( req.perm.has_permission('TICKET_VIEW_REPORTER') or \ req.perm.has_permission('TICKET_VIEW_CC') or \ req.perm.has_permission('TICKET_VIEW_ASSIGNED') ): yield ('pticket', 'Tickets') def get_search_results(self, req, terms, filters): if req.perm.has_permission('TICKET_VIEW'): return if 'pticket' not in filters: return req._MUNGE_FILTER = True fn = PrivateTicketsSystem(self.env).check_ticket_access for result in TicketSystem(self.env).get_search_results(req, terms, ['ticket']): id = int(result[0].split('/')[-1]) self.log.debug('PrivateTicketsSearchModule: Check id %r', id) if fn(req, id): yield result # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, content_type): if hasattr(req, '_MUNGE_FILTER'): node = req.hdf.getObj('search.filters').child() while node: if req.hdf['search.filters.%s.name'%node.name()] == 'pticket': req.hdf['search.filters.%s.name'%node.name()] = 'ticket' node = node.next() return template, content_type privateticketsplugin/0.10/privatetickets/api.py0000775000175000017500000000706010600625642022611 0ustar franciscofranciscofrom trac.core import * from trac.perm import IPermissionRequestor, IPermissionGroupProvider, PermissionSystem from trac.ticket.model import Ticket from trac.config import IntOption, ListOption try: set = set except NameError: from sets import Set as set __all__ = ['PrivateTicketsSystem'] class PrivateTicketsSystem(Component): """Central tasks for the PrivateTickets plugin.""" implements(IPermissionRequestor) group_providers = ExtensionPoint(IPermissionGroupProvider) blacklist = ListOption('privatetickets', 'group_blacklist', default='anonymous, authenticated', doc='Groups that do not affect the common membership check.') # IPermissionRequestor methods def get_permission_actions(self): actions = ['TICKET_VIEW_REPORTER', 'TICKET_VIEW_OWNER', 'TICKET_VIEW_CC'] group_actions = ['TICKET_VIEW_REPORTER_GROUP', 'TICKET_VIEW_OWNER_GROUP', 'TICKET_VIEW_CC_GROUP'] all_actions = actions + [(a+'_GROUP', [a]) for a in actions] return all_actions + [('TICKET_VIEW_SELF', actions), ('TICKET_VIEW_GROUP', group_actions)] # Public methods def check_ticket_access(self, req, id): """Return if this req is permitted access to the given ticket ID.""" try: tkt = Ticket(self.env, id) except TracError: return False # Ticket doesn't exist if req.perm.has_permission('TICKET_VIEW_REPORTER') and \ tkt['reporter'] == req.authname: return True if req.perm.has_permission('TICKET_VIEW_CC') and \ req.authname in [x.strip() for x in tkt['cc'].split(',')]: return True if req.perm.has_permission('TICKET_VIEW_OWNER') and \ req.authname == tkt['owner']: return True if req.perm.has_permission('TICKET_VIEW_REPORTER_GROUP') and \ self._check_group(req.authname, tkt['reporter']): return True if req.perm.has_permission('TICKET_VIEW_OWNER_GROUP') and \ self._check_group(req.authname, tkt['owner']): return True if req.perm.has_permission('TICKET_VIEW_CC_GROUP'): for user in tkt['cc'].split(','): #self.log.debug('Private: CC check: %s, %s', req.authname, user.strip()) if self._check_group(req.authname, user.strip()): return True return False # Internal methods def _check_group(self, user1, user2): """Check if user1 and user2 share a common group.""" user1_groups = self._get_groups(user1) user2_groups = self._get_groups(user2) both = user1_groups.intersection(user2_groups) both -= set(self.blacklist) #self.log.debug('PrivateTicket: %s&%s = (%s)&(%s) = (%s)', user1, user2, ','.join(user1_groups), ','.join(user2_groups), ','.join(both)) return bool(both) def _get_groups(self, user): # Get initial subjects groups = set([user]) for provider in self.group_providers: for group in provider.get_permission_groups(user): groups.add(group) perms = PermissionSystem(self.env).get_all_permissions() repeat = True while repeat: repeat = False for subject, action in perms: if subject in groups and action.islower() and action not in groups: groups.add(action) repeat = True return groups privateticketsplugin/0.10/setup.py0000775000175000017500000000206610772107774020153 0ustar franciscofrancisco#!/usr/bin/env python # -*- coding: iso-8859-1 -*- from setuptools import setup setup( name = 'TracPrivateTickets', version = '1.1.1', packages = ['privatetickets'], #package_data = { 'privatetickets': ['templates/*.cs', 'htdocs/*.js', 'htdocs/*.css' ] }, author = "Noah Kantrowitz", author_email = "noah@coderanger.net", description = "Modified ticket security for Trac.", long_description = "Allow users to only see tickets are involved with.", license = "BSD", keywords = "trac plugin ticket permissions security", url = "http://trac-hacks.org/wiki/PrivateTickets", classifiers = [ 'Framework :: Trac', ], #install_requires = ['TracWebAdmin'], entry_points = { 'trac.plugins': [ 'privatetickets.api = privatetickets.api', 'privatetickets.view = privatetickets.view', 'privatetickets.query = privatetickets.query', 'privatetickets.report = privatetickets.report', 'privatetickets.search = privatetickets.search', ] } ) privateticketsplugin/0.11/0000775000175000017500000000000011533751452016425 5ustar franciscofranciscoprivateticketsplugin/0.11/README0000775000175000017500000000454311017704456017315 0ustar franciscofranciscoNotes ===== Allow users to only see tickets they are associated with. There are three main permissions for this plugin: ``TICKET_VIEW_REPORTER``, ``TICKET_VIEW_CC``, and ``TICKET_VIEW_OWNER``. ``TICKET_VIEW_SELF`` is an alias for all three of these. With each permission, users will only be able to see tickets where they are the person mentioned in the permission. So if a user has ``TICKET_VIEW_REPORTER``, they can only see tickets they reported. For ``TICKET_VIEW_CC``, they just have to be included in the CC list. There are also group-based permissions: ``TICKET_VIEW_REPORTER_GROUP``, ``TICKET_VIEW_CC_GROUP``, and ``TICKET_VIEW_OWNER_GROUP``. These work in a similar way to their non-group counterparts, except that you are granted access if you share a group with the target user. For example, if ticket 1 was reported by Allan, and Allan and Bob are both in the group company_foo, and Bob has ``TICKET_VIEW_REPORTER_GROUP``, then Bob will be able to see ticket 1 since he shares a group with the reporter. Each group-based permission is also an alias for the normal one, so you do not have to grant both. ``TICKET_VIEW_GROUP`` is an alias for all the group-based permissions (and therefore all the normal ones as well). These extra permissions can only deny access, not allow it. This means the user must still have ``TICKET_VIEW`` granted as normal. Finally, users with ``TRAC_ADMIN`` will not be restricted by this plugin. The meta-user "anonymous" also cannot be restricted by this plugin, as their identity isn't known to be checked. Be sure to not grant ``TICKET_VIEW`` to anonymous, or unauthenticated users will be able to see all tickets. Configuration ============= All configuration options go in the ``[privatetickets]`` section. ``group_blacklist`` Groups to ignore for the purposes of the ``*_GROUP`` permissions. Defaults to "``anonymous, authenticated``" You must also add ``PrivateTicketsPolicy`` to your ``permission_policies`` setting in trac.ini. It must be before the ``DefaultPermissionPolicy``. See below for an example if you don't have any other policies. Example ======= An example configuration:: [privatetickets] group_blacklist = anonymous, authenticated, labusers [components] privatetickets.* = enabled [trac] permission_policies = PrivateTicketsPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy privateticketsplugin/0.11/privatetickets/0000775000175000017500000000000011533751452021466 5ustar franciscofranciscoprivateticketsplugin/0.11/privatetickets/__init__.py0000775000175000017500000000000010522011526023554 0ustar franciscofranciscoprivateticketsplugin/0.11/privatetickets/policy.py0000775000175000017500000001144411522302000023321 0ustar franciscofrancisco# Created by Noah Kantrowitz on 2008-04-04. # Copyright (c) 2008 Noah Kantrowitz. All rights reserved. from trac.core import * from trac.perm import IPermissionRequestor, IPermissionGroupProvider, IPermissionPolicy, PermissionSystem from trac.ticket.model import Ticket from trac.config import IntOption, ListOption from trac.util.compat import set class PrivateTicketsPolicy(Component): """Central tasks for the PrivateTickets plugin.""" implements(IPermissionRequestor, IPermissionPolicy) group_providers = ExtensionPoint(IPermissionGroupProvider) blacklist = ListOption('privatetickets', 'group_blacklist', default='anonymous, authenticated', doc='Groups that do not affect the common membership check.') ignore_permissions = set([ 'TRAC_ADMIN', 'TICKET_VIEW_REPORTER', 'TICKET_VIEW_OWNER', 'TICKET_VIEW_CC', 'TICKET_VIEW_REPORTER_GROUP', 'TICKET_VIEW_OWNER_GROUP', 'TICKET_VIEW_CC_GROUP', ]) # IPermissionPolicy(Interface) def check_permission(self, action, username, resource, perm): if username == 'anonymous' or \ action in self.ignore_permissions or \ 'TRAC_ADMIN' in perm: # In these three cases, checking makes no sense return None # Look up the resource parentage for a ticket. while resource: if resource.realm == 'ticket': break resource = resource.parent if resource and resource.realm == 'ticket' and resource.id is not None: return self.check_ticket_access(perm, resource) return None # IPermissionRequestor methods def get_permission_actions(self): actions = ['TICKET_VIEW_REPORTER', 'TICKET_VIEW_OWNER', 'TICKET_VIEW_CC'] group_actions = ['TICKET_VIEW_REPORTER_GROUP', 'TICKET_VIEW_OWNER_GROUP', 'TICKET_VIEW_CC_GROUP'] all_actions = actions + [(a+'_GROUP', [a]) for a in actions] return all_actions + [('TICKET_VIEW_SELF', actions), ('TICKET_VIEW_GROUP', group_actions)] # Public methods def check_ticket_access(self, perm, res): """Return if this req is permitted access to the given ticket ID.""" try: tkt = Ticket(self.env, res.id) except TracError: return None # Ticket doesn't exist had_any = False if perm.has_permission('TICKET_VIEW_REPORTER'): had_any = True if tkt['reporter'] == perm.username: return None if perm.has_permission('TICKET_VIEW_CC'): had_any = True if perm.username in [x.strip() for x in tkt['cc'].split(',')]: return None if perm.has_permission('TICKET_VIEW_OWNER'): had_any = True if perm.username == tkt['owner']: return None if perm.has_permission('TICKET_VIEW_REPORTER_GROUP'): had_any = True if self._check_group(perm.username, tkt['reporter']): return None if perm.has_permission('TICKET_VIEW_OWNER_GROUP'): had_any = True if self._check_group(perm.username, tkt['owner']): return None if perm.has_permission('TICKET_VIEW_CC_GROUP'): had_any = True for user in tkt['cc'].split(','): #self.log.debug('Private: CC check: %s, %s', req.authname, user.strip()) if self._check_group(perm.username, user.strip()): return None # No permissions assigned, punt if not had_any: return None return False # Internal methods def _check_group(self, user1, user2): """Check if user1 and user2 share a common group.""" user1_groups = self._get_groups(user1) user2_groups = self._get_groups(user2) both = user1_groups.intersection(user2_groups) both -= set(self.blacklist) #self.log.debug('PrivateTicket: %s&%s = (%s)&(%s) = (%s)', user1, user2, ','.join(user1_groups), ','.join(user2_groups), ','.join(both)) return bool(both) def _get_groups(self, user): # Get initial subjects groups = set([user]) for provider in self.group_providers: for group in provider.get_permission_groups(user): groups.add(group) perms = PermissionSystem(self.env).get_all_permissions() repeat = True while repeat: repeat = False for subject, action in perms: if subject in groups and not action.isupper() and action not in groups: groups.add(action) repeat = True return groupsprivateticketsplugin/0.11/setup.py0000775000175000017500000000167311522302000020124 0ustar franciscofrancisco#!/usr/bin/env python # -*- coding: iso-8859-1 -*- import os from setuptools import setup setup( name = 'TracPrivateTickets', version = '2.0.3', packages = ['privatetickets'], author = 'Noah Kantrowitz', author_email = 'noah@coderanger.net', description = 'Modified ticket security for Trac.', #long_description = 'Allow users to only see tickets they are involved with.', long_description = open(os.path.join(os.path.dirname(__file__), 'README')).read(), license = 'BSD', keywords = 'trac plugin ticket permissions security', url = 'http://trac-hacks.org/wiki/PrivateTicketsPlugin', download_url = 'http://trac-hacks.org/svn/privateticketsplugin/0.11#egg=TracPrivateTickets-dev', classifiers = [ 'Framework :: Trac', ], install_requires = ['Trac'], entry_points = { 'trac.plugins': [ 'privatetickets.policy = privatetickets.policy', ], }, )