pax_global_header00006660000000000000000000000064117206303350014512gustar00rootroot0000000000000052 comment=cbfffa204a843b55bd1b68f793b29d7469eeb48c trac-sensitivetickets-0.21/000077500000000000000000000000001172063033500157635ustar00rootroot00000000000000trac-sensitivetickets-0.21/sensitivetickets/000077500000000000000000000000001172063033500213635ustar00rootroot00000000000000trac-sensitivetickets-0.21/sensitivetickets/__init__.py000066400000000000000000000000421172063033500234700ustar00rootroot00000000000000# from sensitivetickets import * trac-sensitivetickets-0.21/sensitivetickets/sensitivetickets.py000066400000000000000000000222451172063033500253420ustar00rootroot00000000000000""" SensitiveTicketsPlugin : a plugin for Trac, http://trac.edgewall.org Based on the example vulnerability_tickets plugin, but SensitivityTickets uses a checkbox to control status versus text. See: http://trac.edgewall.org/browser/trunk/sample-plugins/permissions/vulnerability_tickets.py """ from trac.core import * from trac.config import BoolOption from trac.perm import IPermissionPolicy, IPermissionRequestor from trac.env import IEnvironmentSetupParticipant from trac.ticket.model import Ticket from trac.ticket.api import ITicketManipulator from trac.timeline.api import ITimelineEventProvider from trac.resource import ResourceNotFound from datetime import datetime from trac.util.datefmt import format_datetime, from_utimestamp, \ to_utimestamp, utc from trac.util import as_bool class SensitiveTicketsPolicy(Component): """Prevent public access to security sensitive tickets. Add the SENSITIVE_VIEW permission as a pre-requisite for any other permission check done on tickets that have been marked (through the UI) as "Sensitive". Once this plugin is enabled, you'll have to insert it at the appropriate place in your list of permission policies, e.g. {{{ [trac] permission_policies = SensitiveTicketsPolicy, AuthzPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy }}} This plugin also adds the SENSITIVE_ACTIVITY_VIEW permission, which is narrower in scope than SENSITIVE_VIEW. Accounts with SENSITIVE_ACTIVITY_VIEW will be able to see activity on sensitive material in the timeline, but will only be able to identify it by ticket number, comment number, and timestamp. All other content will be redacted. SENSITIVE_ACTIVITY_VIEW can be useful (for example) for providing a notification daemon the ability to tell that some activity happened without leaking the content of that activity. """ implements(IPermissionPolicy, IPermissionRequestor, IEnvironmentSetupParticipant, ITicketManipulator, ITimelineEventProvider) allow_reporter = BoolOption('sensitivetickets', 'allow_reporter', 'false', '''Whether the reporter of a sensitive ticket should have access to that ticket even if they do not have SENSITIVE_VIEW privileges''') allow_cc = BoolOption('sensitivetickets', 'allow_cc', 'false', '''Whether users listed in the cc field of a sensitive ticket should have access to that ticket even if they do not have SENSITIVE_VIEW privileges''') allow_owner = BoolOption('sensitivetickets', 'allow_owner', 'true', '''Whether the owner of a sensitive ticket should have access to that ticket even if they do not have SENSITIVE_VIEW privileges''') limit_sensitivity = BoolOption('sensitivetickets', 'limit_sensitivity', 'false', '''With limit_sensitivity set to true, users cannot set the sensitivity checkbox on a ticket unless they are authenticated and would otherwise be permitted to deal with the ticket if it were marked sensitive. This prevents users from marking the tickets of other users as "sensitive".''') # IPermissionPolicy methods def check_permission(self, action, username, resource, perm): # We add the 'SENSITIVE_VIEW' pre-requisite for any action # other than 'SENSITIVE_VIEW' itself, as this would lead # to recursion. if action == 'SENSITIVE_VIEW': return # Check whether we're dealing with a ticket resource while resource: if resource.realm == 'ticket': break resource = resource.parent if resource and resource.realm == 'ticket' and resource.id is not None: bypass = False try: ticket = Ticket(self.env, int(resource.id)) sensitive = ticket['sensitive'] if as_bool(sensitive): bypass = self.bypass_sensitive_view(ticket, username) except ResourceNotFound: sensitive = 1 # Fail safe to prevent a race condition. if as_bool(sensitive): if 'SENSITIVE_VIEW' not in perm and not bypass: return False # IPermissionRequestor methods def get_permission_actions(self): yield 'SENSITIVE_VIEW' yield 'SENSITIVE_ACTIVITY_VIEW' # ITicketManipulator methods: def validate_ticket(self, req, ticket): if not self.limit_sensitivity: return [] sensitive = 1 try: sensitive = ticket['sensitive'] except: pass if as_bool(sensitive): if req.authname is 'anonymous': return [(None, 'Sorry, you cannot create or update a sensitive ticket without at least logging in first')] if self.bypass_sensitive_view(ticket, req.authname): return [] req.perm(ticket.resource).require('SENSITIVE_VIEW') return [] ### methods for IEnvironmentSetupParticipant """Extension point interface for components that need to participate in the creation and upgrading of Trac environments, for example to create additional database tables.""" def environment_created(self): """Called when a new Trac environment is created.""" if self.environment_needs_upgrade(None): self.upgrade_environment(None) def environment_needs_upgrade(self, db): """Called when Trac checks whether the environment needs to be upgraded. Should return `True` if this participant needs an upgrade to be performed, `False` otherwise. """ return 'sensitive' not in self.config['ticket-custom'] def upgrade_environment(self, db): """Actually perform an environment upgrade. Implementations of this method should not commit any database transactions. This is done implicitly after all participants have performed the upgrades they need without an error being raised. """ if not self.environment_needs_upgrade(db): return custom = self.config['ticket-custom'] custom.set('sensitive','checkbox') custom.set('sensitive.label', "Sensitive") custom.set('sensitive.value', '0') self.config.save() ### ITimelineEventProvider methods: def get_timeline_filters(self, req): if ('SENSITIVE_ACTIVITY_VIEW' in req.perm and 'SENSITIVE_VIEW' not in req.perm): yield ('sensitive_activity', 'Activity on sensitive tickets', False) def get_timeline_events(self, req, start, stop, filters): if ('sensitive_activity' in filters and 'SENSITIVE_ACTIVITY_VIEW' in req.perm and 'SENSITIVE_VIEW' not in req.perm): ts_start = to_utimestamp(start) ts_stop = to_utimestamp(stop) db = self.env.get_db_cnx() cursor = db.cursor() if 'ticket_details' in filters: # only show sensitive ticket changes (edits, closure) if the 'ticket_details' filter is on: cursor.execute(""" SELECT DISTINCT t.id,tc.time,tc.oldvalue FROM ticket_change tc INNER JOIN ticket t ON t.id = tc.ticket AND tc.time >= %s AND tc.time <= %s AND tc.field = %s INNER JOIN ticket_custom td ON t.id = td.ticket AND td.name = %s AND td.value = %s ORDER BY tc.time """, (ts_start, ts_stop, 'comment', 'sensitive', '1')) for tid,t,cid in cursor: yield ('sensitive_activity', from_utimestamp(t), 'redacted', (tid, cid)) # always show new sensitive tickets: cursor.execute(''' SELECT DISTINCT id, time FROM ticket t INNER JOIN ticket_custom tc ON t.id = tc.ticket AND t.time >= %s AND t.time <= %s AND tc.name = %s AND tc.value = %s ORDER BY time ''', (ts_start, ts_stop, 'sensitive', '1')) for tid,t in cursor: yield ('sensitive_activity', from_utimestamp(t), 'redacted', (tid, None)) def render_timeline_event(self, context, field, event): tid, cid = event[3] if field == 'title': return 'Sensitive Activity' elif field == 'description': return '[REDACTED]' elif field == 'url': href = context.href.ticket(tid) if cid: href += '#comment:' + str(cid) return href ### private methods: def bypass_sensitive_view(self, ticket, username): '''Returns whether the sensitivetickets permission allows a bypass of the SENSITIVE_VIEW setting for a given ticket ''' if username == 'anonymous': return False return (self.allow_owner and (ticket['owner'] == username)) or \ (self.allow_reporter and (ticket['reporter'] == username)) or \ (self.allow_cc and (username in [x.strip() for x in ticket['cc'].split(',')])) trac-sensitivetickets-0.21/setup.py000066400000000000000000000023401172063033500174740ustar00rootroot00000000000000from setuptools import find_packages, setup import sys version='0.21' # Check Trac version if installed (see # http://trac-hacks.org/ticket/9800#comment:4) try: import trac if trac.__version__ < '0.11.6': print "SensitiveTickets %s requires Trac >= 0.11.6" % version sys.exit(1) except ImportError: pass setup(name='sensitivetickets', version=version, description="A trac plugin that lets you mark tickets as 'sensitive' with a check box. Those tickets can only be seen with permission. This plugin is based on the example plugin http://trac.edgewall.org/browser/trunk/sample-plugins/permissions/vulnerability_tickets.py", author='Jeff Hammel, Sebastian Benthall, Rowan Wookey, Daniel Kahn Gillmor', author_email='dkg@fifthhorseman.net', maintainer = 'Daniel Kahn Gillmor', maintainer_email = 'dkg@fifthhorseman.net', url='http://trac-hacks.org/wiki/SensitiveTicketsPlugin', keywords='trac plugin security', license="GPL", packages=find_packages(exclude=['ez_setup', 'examples', 'tests*']), include_package_data=True, zip_safe=False, entry_points = """ [trac.plugins] sensitivetickets = sensitivetickets """, )