class CommitMessageCommandParser(object): def __init__(self, env, a_commit_message): self.env = env self.message = a_commit_message # REFACT: rename tm to ticket_model_manager to make the type clearer self.tm = AgiloTicketModelManager(self.env) self.validation_errors = list() self.commands = list() def validate_and_parse_commit_message(self): if '' == self.message: self.abort_with_usage("Please provide a comment") # Now get all the commands and tickets out of the log and check whether are valid or not #print "Message: %s" % repr(self.message) #print 'regex:', COMMAND_AND_TICKET_PATTERN for match in COMMAND_AND_TICKET_REGEX.finditer(self.message): command = match.group('command') #print "Expression matching: <%s> <%s> group(0) <%s>" % (match.group('command'), match.group('ticket'), match.group(0)) for ticket_id, remaining_time in TICKET_REGEX.findall( match.group('ticket')): #print 'command', command, 'ticket_id', ticket_id, 'remaining_time', remaining_time # Check that the ticket is existing first ticket = self.validate_and_get_ticket(ticket_id) if not ticket: continue if self.does_command_require_remaining_time(command): self.validate_is_remaining_time_set( command, remaining_time) self.validate_includes_configured_unit_in_remaining_time( remaining_time) self.validate_can_change_remaining_time_on_ticket( ticket, remaining_time) else: self.validate_no_remaining_time_set( command, remaining_time) self.commands.append((command, ticket_id, remaining_time)) if len(self.validation_errors) > 0: #print >> sys.stderr, "\n".join(self.validation_errors) raise InvalidAttributeError("\n".join(self.validation_errors)) return self.commands def validate_and_get_ticket(self, ticket_id): try: t_id = int(ticket_id.replace('#', '')) ticket = self.tm.get(tkt_id=t_id, load=True) return ticket except Exception, e: self.validation_errors.append( "Unable to verify ticket #%s, reason: '%s'" % (ticket_id, to_unicode(e))) return None
class CommitMessageCommandParser(object): def __init__(self,env, a_commit_message): self.env = env self.message = a_commit_message # REFACT: rename tm to ticket_model_manager to make the type clearer self.tm = AgiloTicketModelManager(self.env) self.validation_errors = list() self.commands = list() def validate_and_parse_commit_message(self): if '' == self.message: self.abort_with_usage("Please provide a comment") # Now get all the commands and tickets out of the log and check whether are valid or not #print "Message: %s" % repr(self.message) #print 'regex:', COMMAND_AND_TICKET_PATTERN for match in COMMAND_AND_TICKET_REGEX.finditer(self.message): command = match.group('command') #print "Expression matching: <%s> <%s> group(0) <%s>" % (match.group('command'), match.group('ticket'), match.group(0)) for ticket_id, remaining_time in TICKET_REGEX.findall(match.group('ticket')): #print 'command', command, 'ticket_id', ticket_id, 'remaining_time', remaining_time # Check that the ticket is existing first ticket = self.validate_and_get_ticket(ticket_id) if not ticket: continue if self.does_command_require_remaining_time(command): self.validate_is_remaining_time_set(command, remaining_time) self.validate_includes_configured_unit_in_remaining_time(remaining_time) self.validate_can_change_remaining_time_on_ticket(ticket, remaining_time) else: self.validate_no_remaining_time_set(command, remaining_time) self.commands.append((command, ticket_id, remaining_time)) if len(self.validation_errors) > 0: #print >> sys.stderr, "\n".join(self.validation_errors) raise InvalidAttributeError("\n".join(self.validation_errors)) return self.commands def validate_and_get_ticket(self, ticket_id): try: t_id = int(ticket_id.replace('#', '')) ticket = self.tm.get(tkt_id=t_id, load=True) return ticket except Exception, e: self.validation_errors.append("Unable to verify ticket #%s, reason: '%s'" % (ticket_id, to_unicode(e))) return None
def testTicketWithModelManager(self): """Tests the ticket creation via ModelManager""" from agilo.ticket.model import AgiloTicketModelManager manager = AgiloTicketModelManager(self.env) t1 = manager.create(summary='This is a test ticket', t_type=Type.TASK, remaining_time='12', description='take this') self.assert_true(t1.exists) self.assertNotEqual(0, t1.id) self.assert_equals('This is a test ticket', t1[Key.SUMMARY]) self.assert_equals('12', t1[Key.REMAINING_TIME]) self.assert_equals('take this', t1[Key.DESCRIPTION]) t2 = manager.get(tkt_id=t1.id) self.assert_true(t2.exists) self.assert_equals(t1.id, t2.id) # test cache too self.assert_equals(id(t1), id(t2)) # Now change the summary t2.summary = 'A new summary' manager.save(t2, author='tester', comment='Updated summary...') # is the same object so... self.assert_equals(t2.summary, t1.summary)
def testTicketWithModelManager(self): """Tests the ticket creation via ModelManager""" from agilo.ticket.model import AgiloTicketModelManager manager = AgiloTicketModelManager(self.env) t1 = manager.create(summary='This is a test ticket', t_type=Type.TASK, remaining_time='12', description='take this') self.assert_true(t1.exists) self.assertNotEqual(0, t1.id) self.assert_equals('This is a test ticket', t1[Key.SUMMARY]) self.assert_equals('12', t1[Key.REMAINING_TIME]) self.assert_equals('take this', t1[Key.DESCRIPTION]) t2 = manager.get(tkt_id=t1.id) self.assert_true(t2.exists) self.assert_equals(t1.id, t2.id) # test cache too self.assert_equals(id(t1), id(t2)) # Now change the summary t2.summary = 'A new summary' manager.save(t2, author='tester', comment='Updated summary...') # is the same object so... self.assert_equals(t2.summary, t1.summary)
def process_request(self, req): # Check if it has been called with 'src' and 'dest' arguments or abort if not req.args.has_key('src') or not req.args.has_key( 'dest') or not req.args.has_key('cmd'): raise TracError( "Links should be called with 'cmd', 'src' and 'dest' parameters", "Links: Source and/or Destination are Missing!") else: # Flag for ticket update update_ticket = False # Avoid recursive imports from agilo.ticket.model import AgiloTicketModelManager tm = AgiloTicketModelManager(self.env) try: src = int(req.args.get('src')) # Now that we have the ticket we can check permission to link or edit ticket_perm = req.perm('ticket', src) if Action.TICKET_EDIT not in ticket_perm or \ Action.LINK_EDIT not in ticket_perm: raise TracError("You (%s) are not authorized to edit this ticket" % \ req.authname) dest = int(req.args.get('dest')) cmd = req.args.get('cmd') url = req.args.get('url_orig') or None except: raise TracError("Source is not valid: src=%s" % req.args.get('src')) # Create the LinkEndPoint for the source and destination if not existing sle = tm.get(tkt_id=src) dle = tm.get(tkt_id=dest) src_type = sle.get_type() dest_type = dle.get_type() if cmd == 'create link': if not sle.link_to(dle): raise TracError("Links not allowed between %s->%s! The types are incompatible or" \ " the link already exists" % (src_type, dest_type)) else: req.args[Key.OUTGOING_LINKS] = 'created link %s(#%s)->%s(#%s)' % \ (src_type, src, dest_type, dest) req.args['comment'] = 'Added link to %s(#%s)' % \ (dest_type, dest) update_ticket = True elif cmd == 'delete link': if not sle.del_link_to(dle): raise TracError("Link not existing! %s(#%s)->%s(#%s)" % \ (src_type, src, dest_type, dest)) else: req.args[Key.OUTGOING_LINKS] = 'deleted link %s(#%s)->%s(#%s)' % \ (src_type, src, dest_type, dest) req.args['comment'] = 'Deleted link to %s(#%s)' % ( dest_type, dest) update_ticket = True else: raise TracError("ERROR: Unknown Command %s!" % cmd) #set the link into the request and let TicketWrapper update the custom field if update_ticket: req.args['id'] = src req.args['summary'] = sle[Key.SUMMARY] req.args['ts'] = sle.time_changed req.args['action'] = 'leave' # Redirect to original /ticket url to avoid any change in existing view req.redirect(url or req.base_url)
class CSVBasePerformer(object): """Base Performer class for CSV parsing""" def __init__(self, env, number_rows_for_preview=20): self.do_preview = False self.rows = [] self.number_rows_for_preview = number_rows_for_preview self.env = env self.tm = AgiloTicketModelManager(self.env) def commit(self, req): '''Perform the operation for all processed rows. Return a list of new or changed tickets''' raise NotImplementedError def check_header(self, header): """Check the headers for mandatory fields""" raise NotImplementedError def get_preview_rows(self): assert self.do_preview return self.rows def interesting_fieldnames(self): '''Return a list of field names which this performer will handle.''' raise NotImplementedError def name(self): 'Return a human readable name of the performer.' return self.__class__.__name__ def process(self, fields): '''Process the given row and save it for a later commit.''' if fields not in [None, {}]: if self.do_preview: if len(self.rows) < self.number_rows_for_preview: self.rows.append(fields) else: self.rows.append(fields) def set_preview_mode(self): '''Tell the performer that it should only parse the lines for preview mode - therefore no data is changed.''' self.do_preview = True def _get_type_for_ticket(self, fields): type = Type.REQUIREMENT if Key.TYPE in fields: type = fields.pop(Key.TYPE).lower() return type def _may_create_ticket(self, perm, ticket_type): action_name = 'CREATE_%s' % ticket_type.upper() if hasattr(Action, action_name): permission = getattr(Action, action_name) return (permission in perm) return False # should be action_name in perm? def _get_ticket_from_id_in_csv(self, req, fields): ticket = None string_id = fields.get(Key.ID, fields.get('ticket')) try: ticket_id = int(string_id) except (ValueError, TypeError): add_warning(req, _("Non-numeric ticket ID '%s'") % string_id) else: try: ticket = self.tm.get(tkt_id=int(ticket_id)) except ResourceNotFound: add_warning(req, _("Ticket %d does not exist") % ticket_id) return ticket
class CSVBasePerformer(object): """Base Performer class for CSV parsing""" def __init__(self, env, number_rows_for_preview=20): self.do_preview = False self.rows = [] self.number_rows_for_preview = number_rows_for_preview self.env = env self.tm = AgiloTicketModelManager(self.env) def commit(self, req): '''Perform the operation for all processed rows. Return a list of new or changed tickets''' raise NotImplementedError def check_header(self, header): """Check the headers for mandatory fields""" raise NotImplementedError def get_preview_rows(self): assert self.do_preview return self.rows def interesting_fieldnames(self): '''Return a list of field names which this performer will handle.''' raise NotImplementedError def name(self): 'Return a human readable name of the performer.' return self.__class__.__name__ def process(self, fields): '''Process the given row and save it for a later commit.''' if fields not in [None, {}]: if self.do_preview: if len(self.rows) < self.number_rows_for_preview: self.rows.append(fields) else: self.rows.append(fields) def set_preview_mode(self): '''Tell the performer that it should only parse the lines for preview mode - therefore no data is changed.''' self.do_preview = True def _get_type_for_ticket(self, fields): type = Type.REQUIREMENT if Key.TYPE in fields: type = fields.pop(Key.TYPE).lower() return type def _may_create_ticket(self, perm, ticket_type): action_name = 'CREATE_%s' % ticket_type.upper() if hasattr(Action, action_name): permission = getattr(Action, action_name) return (permission in perm) return False # should be action_name in perm? def _get_ticket_from_id_in_csv(self, req, fields): ticket = None string_id = fields.get(Key.ID, fields.get('ticket')) try: ticket_id = int(string_id) except (ValueError, TypeError): add_warning(req, _("Non-numeric ticket ID '%s'") % string_id) else: try: ticket = self.tm.get(tkt_id=int(ticket_id)) except ResourceNotFound: add_warning(req, _("Ticket %d does not exist") % ticket_id) return ticket
class TestTicketModelManager(AgiloTestCase): """Tests AgiloTicket ModelManager, in particular the select method""" def setUp(self): self.super() self.manager = AgiloTicketModelManager(self.env) def test_create_ticket(self): """Tests the creation of a ticket using the ModelManager""" t = self.manager.create(summary="This is a ticket") self.assert_true(t.exists, "Ticket not existing...") self.assert_equals("This is a ticket", t[Key.SUMMARY]) # Create without saving t2 = self.manager.create(summary="Not saved", save=False) self.assert_false(t2.exists, "The ticket has been saved!") self.assert_equals("Not saved", t2[Key.SUMMARY]) # Now add something and change the summary t2[Key.DESCRIPTION] = "changed" t2[Key.SUMMARY] = "Now saved" self.manager.save(t2) self.assert_true(t2.exists) self.assert_equals("changed", t2[Key.DESCRIPTION]) self.assert_equals("Now saved", t2[Key.SUMMARY]) def test_ticket_caching(self): """Tests the ticket caching""" t1 = self.manager.create(summary="Ticket #1", t_type=Type.USER_STORY) t1_dupe = self.manager.get(tkt_id=t1.id) self.assert_equals(t1, t1_dupe) def test_select_tickets(self): """Tests the select method to get tickets""" milestone = self.teh.create_milestone('Test') sprint = self.teh.create_sprint('Test', milestone=milestone) t1 = self.manager.create(summary="Ticket #1", t_type=Type.USER_STORY, sprint=sprint.name) t2 = self.manager.create(summary="Ticket #2", t_type=Type.TASK) # Now the plan select should return both tickets tickets = self.manager.select() self.assert_true(t1 in tickets, "T1 is not in tickets!?") self.assert_true(t2 in tickets, "T2 is not in tickets!?") # Now selects all tickets planned for sprint Test self.assert_equals(sprint.name, t1[Key.SPRINT]) tickets = self.manager.select(criteria={Key.SPRINT: 'Test'}) self.assert_true(t1 in tickets, "T1 is not in ticket!?") self.assert_false(t2 in tickets, "T2 is in tickets and should not?!") # Now selects all tickets planned for milestone Test self.assert_equals('Test', t1[Key.MILESTONE]) tickets = self.manager.select(criteria={Key.MILESTONE: 'Test'}) self.assert_true(t1 in tickets, "T1 is not in tickets!?") self.assert_false(t2 in tickets, "T2 is in tickets and should not?!") # Now tests the select with a limit to 1 tickets = self.manager.select(limit=1) self.assert_equals(1, len(tickets)) tickets = self.manager.select(limit=2) self.assert_equals(2, len(tickets)) # Now select all the tickets that have been created before now tickets = self.manager.select(criteria={'changetime': '<=%s' % \ t1.time_created}) self.assert_equals(2, len(tickets)) # Now try out the order by tickets = self.manager.select(order_by=['-sprint']) self.assert_equals(tickets[0], t1) self.assert_equals(tickets[1], t2) def test_criteria_not_split_if_no_type(self): """Tests the splitting of the criteria in the selct query when containing the paramater ticket type""" criteria = { 'summary': 'test', Key.REMAINING_TIME: '2', 'id': 'not in (1, 2, 3)' } self.assert_none(self.manager._split_ticket_type(criteria)) def test_criteria_split_if_type(self): """Tests the splitting of the criteria in the selct query when containing the paramater ticket type""" criteria = { 'summary': 'test', Key.REMAINING_TIME: '2', 'type': "in ('story', 'task')" } res = self.manager._split_ticket_type(criteria) self.assert_not_none(res) self.assert_equals('story', res[0].value) self.assert_equals('=', res[0].operator) self.assert_equals(('task', 'story'), res[1].value) self.assert_equals('in', res[1].operator)
class TestTicketModelManager(AgiloTestCase): """Tests AgiloTicket ModelManager, in particular the select method""" def setUp(self): self.super() self.manager = AgiloTicketModelManager(self.env) def test_create_ticket(self): """Tests the creation of a ticket using the ModelManager""" t = self.manager.create(summary="This is a ticket") self.assert_true(t.exists, "Ticket not existing...") self.assert_equals("This is a ticket", t[Key.SUMMARY]) # Create without saving t2 = self.manager.create(summary="Not saved", save=False) self.assert_false(t2.exists, "The ticket has been saved!") self.assert_equals("Not saved", t2[Key.SUMMARY]) # Now add something and change the summary t2[Key.DESCRIPTION] = "changed" t2[Key.SUMMARY] = "Now saved" self.manager.save(t2) self.assert_true(t2.exists) self.assert_equals("changed", t2[Key.DESCRIPTION]) self.assert_equals("Now saved", t2[Key.SUMMARY]) def test_ticket_caching(self): """Tests the ticket caching""" t1 = self.manager.create(summary="Ticket #1", t_type=Type.USER_STORY) t1_dupe = self.manager.get(tkt_id=t1.id) self.assert_equals(t1, t1_dupe) def test_select_tickets(self): """Tests the select method to get tickets""" milestone = self.teh.create_milestone('Test') sprint = self.teh.create_sprint('Test', milestone=milestone) t1 = self.manager.create(summary="Ticket #1", t_type=Type.USER_STORY, sprint=sprint.name) t2 = self.manager.create(summary="Ticket #2", t_type=Type.TASK) # Now the plan select should return both tickets tickets = self.manager.select() self.assert_true(t1 in tickets, "T1 is not in tickets!?") self.assert_true(t2 in tickets, "T2 is not in tickets!?") # Now selects all tickets planned for sprint Test self.assert_equals(sprint.name, t1[Key.SPRINT]) tickets = self.manager.select(criteria={Key.SPRINT: 'Test'}) self.assert_true(t1 in tickets, "T1 is not in ticket!?") self.assert_false(t2 in tickets, "T2 is in tickets and should not?!") # Now selects all tickets planned for milestone Test self.assert_equals('Test', t1[Key.MILESTONE]) tickets = self.manager.select(criteria={Key.MILESTONE: 'Test'}) self.assert_true(t1 in tickets, "T1 is not in tickets!?") self.assert_false(t2 in tickets, "T2 is in tickets and should not?!") # Now tests the select with a limit to 1 tickets = self.manager.select(limit=1) self.assert_equals(1, len(tickets)) tickets = self.manager.select(limit=2) self.assert_equals(2, len(tickets)) # Now select all the tickets that have been created before now tickets = self.manager.select(criteria={'changetime': '<=%s' % \ t1.time_created}) self.assert_equals(2, len(tickets)) # Now try out the order by tickets = self.manager.select(order_by=['-sprint']) self.assert_equals(tickets[0], t1) self.assert_equals(tickets[1], t2) def test_criteria_not_split_if_no_type(self): """Tests the splitting of the criteria in the selct query when containing the paramater ticket type""" criteria = {'summary': 'test', Key.REMAINING_TIME: '2', 'id': 'not in (1, 2, 3)'} self.assert_none(self.manager._split_ticket_type(criteria)) def test_criteria_split_if_type(self): """Tests the splitting of the criteria in the selct query when containing the paramater ticket type""" criteria = {'summary': 'test', Key.REMAINING_TIME: '2', 'type': "in ('story', 'task')"} res = self.manager._split_ticket_type(criteria) self.assert_not_none(res) self.assert_equals('story', res[0].value) self.assert_equals('=', res[0].operator) self.assert_equals(('task', 'story'), res[1].value) self.assert_equals('in', res[1].operator)
class AgiloPolicy(Component): """ Check access to all Agilo resources against the corresponding roles. """ implements(IPermissionPolicy) def __init__(self): self.action_map = { Action.ATTACHMENT_CREATE: self.check_attachment_create, Action.CONFIRM_COMMITMENT: self.confirm_commitment, Action.TICKET_EDIT: self.check_ticket_edit, # We need also to catch trac's Actions for modifying tickets # - otherwise too many things will be allowed! Action.TICKET_CHANGE: self.check_ticket_edit, Action.TICKET_MODIFY: self.check_ticket_edit, Action.TICKET_EDIT_PAGE_ACCESS: self.check_ticket_edit_page_access, Action.TICKET_EDIT_DESCRIPTION: self.check_edit_description, # We don't have add a check for TICKET_APPEND because all three # roles get the TICKET_APPEND permission via meta permissions. # Therefore checking for team members is useless. # Action.TICKET_APPEND: self.check_ticket_append, Action.LINK_EDIT: self.check_link_edit, Action.SAVE_REMAINING_TIME: self.check_save_remaining_time, Action.BACKLOG_EDIT: self.check_backlog_edit, } self.tm = AgiloTicketModelManager(self.env) # --- permission checks ---------------------------------------------------- # REFACT: Clean up the whole permission implementation. # Currently it's confusing + inconsistent with CREATE_... and edit. def check_ticket_edit(self, username, resource, perm, t_type=None): """ Check agilo ticket edit permissions, the schema should be as follows: Action.PRODUCT_OWNER: can edit Type.REQUIREMENT, Type.USER_STORY Action.SCRUM_MASTER: can link Type.USER_STORY, edit Type.TASK Action.TEAM_MEMBER: can link Type.USER_STORY, edit own Type.TASK or unassigned Type.TASK, or Type.TASK where is a Key.RESOURCE. The permission try to be as much loose as possible to allow customization the idea is to extend Type.USER_STORY to any Type.TASK container. """ # check if is admin cause we don't need to check anything else if Action.TRAC_ADMIN in perm or Action.TICKET_ADMIN in perm: return True if not resource and t_type is None: return None ticket, t_type = self._get_ticket_and_type(resource, t_type) if t_type is None: return None if t_type not in (Type.TASK, Type.USER_STORY, Type.REQUIREMENT, Type.BUG): # We don't make any assumptions about ticket types which are not # part of the Agilo Core. return None if t_type == Type.BUG: return perm.has_permission(Action.CREATE_BUG, resource) # Task is our leaf, if we would consider task everything with Remaining Time # we may still encounter a type like Spike, with remaining time, but indeed # a potential task container. ticket_is_task = t_type == Type.TASK # We can't make it more loose is_team_member = Role.TEAM_MEMBER in perm is_scrum_master = Role.SCRUM_MASTER in perm is_ticket_owner = is_reporter = False if ticket.get_value() is not None: is_ticket_owner = (username == ticket[Key.OWNER]) or \ (username in ticket.get_resource_list()) is_reporter = (username == ticket[Key.REPORTER]) ticket_has_no_owner = (ticket.get_value() is None or not ticket[Key.OWNER]) is_product_owner = (Role.PRODUCT_OWNER in perm) if is_ticket_owner or \ (ticket_is_task and (is_team_member or is_reporter) and ticket_has_no_owner) or \ (is_product_owner and not ticket_is_task) or \ (ticket_is_task and is_scrum_master): return True # We must stop the default policy which does not know anything about # types and does not check owners. Therefore anyone with TICKET_EDIT # permission could edit tickets if we don't deny here! return False def check_ticket_edit_page_access(self, username, resource, perm): def can_create_at_least_one_referenced_type(ticket_type): for allowed_type in LinksConfiguration(self.env).get_allowed_destination_types(ticket_type): permission_name = CoreTemplateProvider(self.env).get_permission_name_to_create(allowed_type) if permission_name in perm: return True return False ticket_type = self._get_ticket_type(resource) if ticket_type is None: return True if can_create_at_least_one_referenced_type(ticket_type): return True return perm.has_permission(Action.TICKET_MODIFY, resource) def check_edit_description(self, username, resource, perm): # For now we just ignore trac's TICKET_EDIT_DESCRIPTION privileges return self.check_ticket_edit(username, resource, perm) def check_attachment_create(self, username, resource, perm): # The idea is that any user with ATTACHMENT_CREATE can create # attachments *if* she is allowed to edit the ticket. This check # places additional constraints on ATTACHMENT_CREATE: It denies access # for users who may not edit the ticket but leaves the final decision # to other trac policies. ticket = self._get_agilo_ticket(resource) if not ticket: return may_edit_ticket = self.check_ticket_edit(username, resource, perm) if may_edit_ticket == False: return False return None def check_link_edit(self, username, resource, perm): """ Checks it the current user can edit links on the given resource. The rules are: - Product Owner can link every ticket which is not a task, this means that the Product Owner will not be allowed to create tasks - Team Member and Scrum Master, can create link only to task, this means they can edit link on task containers This all works fine because the links can only be created from container to destination, therefore there is no problem with the direction. """ is_product_owner = Role.PRODUCT_OWNER in perm is_tm_or_sm = Role.SCRUM_MASTER in perm or Role.TEAM_MEMBER in perm ticket = self._get_agilo_ticket(resource) is_task_container = False if ticket is not None: is_task_container = Type.TASK in [al.get_dest_type() for al in ticket.get_alloweds()] return (is_product_owner and not is_task_container) or \ (is_tm_or_sm and is_task_container) or \ None def check_backlog_edit(self, username, resource, perm): """ Checks if the current user can edit the given Backlog Resource. The rules are: - Product Backlog: only Product Owner can edit - Sprint Backlog: only Scrum Master can fully edit, team member will have rights on individual tickets (see ticket_edit) - Other Backlog: every authenticated user, as we can't make any other assumption. """ name = self._get_backlog_name(resource) is_product_owner = Role.PRODUCT_OWNER in perm if is_product_owner and (name in (None, Key.PRODUCT_BACKLOG)): return True is_scrum_master = Role.SCRUM_MASTER in perm if is_scrum_master and (name in (None, Key.SPRINT_BACKLOG)): return True if name is None: return None is_custom_backlog = name not in (Key.PRODUCT_BACKLOG, Key.SPRINT_BACKLOG) is_authenticated_user = username is not None and username != 'anonymous' if is_custom_backlog and is_authenticated_user: return True return None def check_save_remaining_time(self, username, resource, perm): """ Checks if the current user can change the remaining time on the given ticket resource. The rules are: - Scrum Master: can always change the remaining time - Team Member: can change remaining time only if owner or resource of the given task, or the task has not yet been assigned, in which case the current user will become also owner. """ is_scrum_master = Role.SCRUM_MASTER in perm if not is_scrum_master: return self.check_ticket_edit(username, resource, perm) return True def sprint(self, sprint_name): return SprintModelManager(self.env).get(name=sprint_name) def _has_sprint_started_more_than_one_day_ago(self, sprint): return now() - sprint.start > timedelta(days=1) def confirm_commitment(self, username, resource, perm): if (resource is None) or resource.realm != Realm.SPRINT: return None sprint = self.sprint(resource.id) if sprint is None: return None if sprint.team is None: # Actually this check is not really a policy decision (but technical # necessity) - however it makes some other code simpler return False if self._has_sprint_started_more_than_one_day_ago(sprint): # TODO: Maybe TRAC_ADMIN should be able to do it anyway? return False return None # IPermissionPolicy methods def check_permission(self, action, username, resource, perm): """ Check that the action can be performed by username on the resource :param action: the name of the permission :param username: the username string or 'anonymous' if there's no authenticated user :param resource: the resource on which the check applies. Will be `None`, if the check is a global one and not made on a resource in particular :param perm: the permission cache for that username and resource, which can be used for doing secondary checks on other permissions. Care must be taken to avoid recursion. :return: `True` if action is allowed, `False` if action is denied, or `None` if indifferent. If `None` is returned, the next policy in the chain will be used, and so on. """ # run the function associated with this action if action in self.action_map: return self.action_map[action](username, resource, perm) # Return None because we don't care about this action so other # PermissionPolicies can vote on this request. return None # --- helpers -------------------------------------------------------------. def _get_resource_id_with_realm(self, resource, realm): while resource: if resource.realm == realm: break resource = resource.parent if not resource or (resource.realm != realm) or (resource.id is None): return None return resource.id def _get_agilo_ticket(self, resource): resource_id = self._get_resource_id_with_realm(resource, Realm.TICKET) return self.tm.get(tkt_id=resource_id) def _get_backlog_name(self, resource): return self._get_resource_id_with_realm(resource, Realm.BACKLOG) def _get_ticket_and_type(self, resource=None, ticket_type=None): ticket = LazyProxy(lambda: self._get_agilo_ticket(resource)) if ticket_type is None: if ticket.get_value() is None: return None, None ticket_type = ticket.get_type() if ticket_type is not None: return ticket, ticket_type return None, None def _get_ticket_type(self, resource): ticket = self._get_agilo_ticket(resource) if ticket is not None: return ticket.get_type() return None
def process_request(self, req): # Check if it has been called with 'src' and 'dest' arguments or abort if not req.args.has_key('src') or not req.args.has_key('dest') or not req.args.has_key('cmd'): raise TracError("Links should be called with 'cmd', 'src' and 'dest' parameters", "Links: Source and/or Destination are Missing!") else: # Flag for ticket update update_ticket = False # Avoid recursive imports from agilo.ticket.model import AgiloTicketModelManager tm = AgiloTicketModelManager(self.env) try: src = int(req.args.get('src')) # Now that we have the ticket we can check permission to link or edit ticket_perm = req.perm('ticket', src) if Action.TICKET_EDIT not in ticket_perm or \ Action.LINK_EDIT not in ticket_perm: raise TracError("You (%s) are not authorized to edit this ticket" % \ req.authname) dest = int(req.args.get('dest')) cmd = req.args.get('cmd') url = req.args.get('url_orig') or None except: raise TracError("Source is not valid: src=%s" % req.args.get('src')) # Create the LinkEndPoint for the source and destination if not existing sle = tm.get(tkt_id=src) dle = tm.get(tkt_id=dest) src_type = sle.get_type() dest_type = dle.get_type() if cmd == 'create link': if not sle.link_to(dle): raise TracError("Links not allowed between %s->%s! The types are incompatible or" \ " the link already exists" % (src_type, dest_type)) else: req.args[Key.OUTGOING_LINKS] = 'created link %s(#%s)->%s(#%s)' % \ (src_type, src, dest_type, dest) req.args['comment'] = 'Added link to %s(#%s)' % \ (dest_type, dest) update_ticket = True elif cmd == 'delete link': if not sle.del_link_to(dle): raise TracError("Link not existing! %s(#%s)->%s(#%s)" % \ (src_type, src, dest_type, dest)) else: req.args[Key.OUTGOING_LINKS] = 'deleted link %s(#%s)->%s(#%s)' % \ (src_type, src, dest_type, dest) req.args['comment'] = 'Deleted link to %s(#%s)' % (dest_type, dest) update_ticket = True else: raise TracError("ERROR: Unknown Command %s!" % cmd) #set the link into the request and let TicketWrapper update the custom field if update_ticket: req.args['id'] = src req.args['summary'] = sle[Key.SUMMARY] req.args['ts'] = sle.time_changed req.args['action'] = 'leave' # Redirect to original /ticket url to avoid any change in existing view req.redirect(url or req.base_url)
class AgiloPolicy(Component): """ Check access to all Agilo resources against the corresponding roles. """ implements(IPermissionPolicy) def __init__(self): self.action_map = { Action.ATTACHMENT_CREATE: self.check_attachment_create, Action.CONFIRM_COMMITMENT: self.confirm_commitment, Action.TICKET_EDIT: self.check_ticket_edit, # We need also to catch trac's Actions for modifying tickets # - otherwise too many things will be allowed! Action.TICKET_CHANGE: self.check_ticket_edit, Action.TICKET_MODIFY: self.check_ticket_edit, Action.TICKET_EDIT_PAGE_ACCESS: self.check_ticket_edit_page_access, Action.TICKET_EDIT_DESCRIPTION: self.check_edit_description, # We don't have add a check for TICKET_APPEND because all three # roles get the TICKET_APPEND permission via meta permissions. # Therefore checking for team members is useless. # Action.TICKET_APPEND: self.check_ticket_append, Action.LINK_EDIT: self.check_link_edit, Action.SAVE_REMAINING_TIME: self.check_save_remaining_time, Action.BACKLOG_EDIT: self.check_backlog_edit, } self.tm = AgiloTicketModelManager(self.env) # --- permission checks ---------------------------------------------------- # REFACT: Clean up the whole permission implementation. # Currently it's confusing + inconsistent with CREATE_... and edit. def check_ticket_edit(self, username, resource, perm, t_type=None): """ Check agilo ticket edit permissions, the schema should be as follows: Action.PRODUCT_OWNER: can edit Type.REQUIREMENT, Type.USER_STORY Action.SCRUM_MASTER: can link Type.USER_STORY, edit Type.TASK Action.TEAM_MEMBER: can link Type.USER_STORY, edit own Type.TASK or unassigned Type.TASK, or Type.TASK where is a Key.RESOURCE. The permission try to be as much loose as possible to allow customization the idea is to extend Type.USER_STORY to any Type.TASK container. """ # check if is admin cause we don't need to check anything else if Action.TRAC_ADMIN in perm or Action.TICKET_ADMIN in perm: return True if not resource and t_type is None: return None ticket, t_type = self._get_ticket_and_type(resource, t_type) if t_type is None: return None if t_type not in (Type.TASK, Type.USER_STORY, Type.REQUIREMENT, Type.BUG): # We don't make any assumptions about ticket types which are not # part of the Agilo Core. return None if t_type == Type.BUG: return perm.has_permission(Action.CREATE_BUG, resource) # Task is our leaf, if we would consider task everything with Remaining Time # we may still encounter a type like Spike, with remaining time, but indeed # a potential task container. ticket_is_task = t_type == Type.TASK # We can't make it more loose is_team_member = Role.TEAM_MEMBER in perm is_scrum_master = Role.SCRUM_MASTER in perm is_ticket_owner = is_reporter = False if ticket.get_value() is not None: is_ticket_owner = (username == ticket[Key.OWNER]) or (username in ticket.get_resource_list()) is_reporter = username == ticket[Key.REPORTER] ticket_has_no_owner = ticket.get_value() is None or not ticket[Key.OWNER] is_product_owner = Role.PRODUCT_OWNER in perm if ( is_ticket_owner or (ticket_is_task and (is_team_member or is_reporter) and ticket_has_no_owner) or (is_product_owner and not ticket_is_task) or (ticket_is_task and is_scrum_master) ): return True # We must stop the default policy which does not know anything about # types and does not check owners. Therefore anyone with TICKET_EDIT # permission could edit tickets if we don't deny here! return False def check_ticket_edit_page_access(self, username, resource, perm): def can_create_at_least_one_referenced_type(ticket_type): for allowed_type in LinksConfiguration(self.env).get_allowed_destination_types(ticket_type): permission_name = CoreTemplateProvider(self.env).get_permission_name_to_create(allowed_type) if permission_name in perm: return True return False ticket_type = self._get_ticket_type(resource) if ticket_type is None: return True if can_create_at_least_one_referenced_type(ticket_type): return True return perm.has_permission(Action.TICKET_MODIFY, resource) def check_edit_description(self, username, resource, perm): # For now we just ignore trac's TICKET_EDIT_DESCRIPTION privileges return self.check_ticket_edit(username, resource, perm) def check_attachment_create(self, username, resource, perm): # The idea is that any user with ATTACHMENT_CREATE can create # attachments *if* she is allowed to edit the ticket. This check # places additional constraints on ATTACHMENT_CREATE: It denies access # for users who may not edit the ticket but leaves the final decision # to other trac policies. ticket = self._get_agilo_ticket(resource) if not ticket: return may_edit_ticket = self.check_ticket_edit(username, resource, perm) if may_edit_ticket == False: return False return None def check_link_edit(self, username, resource, perm): """ Checks it the current user can edit links on the given resource. The rules are: - Product Owner can link every ticket which is not a task, this means that the Product Owner will not be allowed to create tasks - Team Member and Scrum Master, can create link only to task, this means they can edit link on task containers This all works fine because the links can only be created from container to destination, therefore there is no problem with the direction. """ is_product_owner = Role.PRODUCT_OWNER in perm is_tm_or_sm = Role.SCRUM_MASTER in perm or Role.TEAM_MEMBER in perm ticket = self._get_agilo_ticket(resource) is_task_container = False if ticket is not None: is_task_container = Type.TASK in [al.get_dest_type() for al in ticket.get_alloweds()] return (is_product_owner and not is_task_container) or (is_tm_or_sm and is_task_container) or None def check_backlog_edit(self, username, resource, perm): """ Checks if the current user can edit the given Backlog Resource. The rules are: - Product Backlog: only Product Owner can edit - Sprint Backlog: only Scrum Master can fully edit, team member will have rights on individual tickets (see ticket_edit) - Other Backlog: every authenticated user, as we can't make any other assumption. """ name = self._get_backlog_name(resource) is_product_owner = Role.PRODUCT_OWNER in perm if is_product_owner and (name in (None, Key.PRODUCT_BACKLOG)): return True is_scrum_master = Role.SCRUM_MASTER in perm if is_scrum_master and (name in (None, Key.SPRINT_BACKLOG)): return True if name is None: return None is_custom_backlog = name not in (Key.PRODUCT_BACKLOG, Key.SPRINT_BACKLOG) is_authenticated_user = username is not None and username != "anonymous" if is_custom_backlog and is_authenticated_user: return True return None def check_save_remaining_time(self, username, resource, perm): """ Checks if the current user can change the remaining time on the given ticket resource. The rules are: - Scrum Master: can always change the remaining time - Team Member: can change remaining time only if owner or resource of the given task, or the task has not yet been assigned, in which case the current user will become also owner. """ is_scrum_master = Role.SCRUM_MASTER in perm if not is_scrum_master: return self.check_ticket_edit(username, resource, perm) return True def sprint(self, sprint_name): return SprintModelManager(self.env).get(name=sprint_name) def _has_sprint_started_more_than_one_day_ago(self, sprint): return now() - sprint.start > timedelta(days=1) def confirm_commitment(self, username, resource, perm): if (resource is None) or resource.realm != Realm.SPRINT: return None sprint = self.sprint(resource.id) if sprint is None: return None if sprint.team is None: # Actually this check is not really a policy decision (but technical # necessity) - however it makes some other code simpler return False if self._has_sprint_started_more_than_one_day_ago(sprint): # TODO: Maybe TRAC_ADMIN should be able to do it anyway? return False return None # IPermissionPolicy methods def check_permission(self, action, username, resource, perm): """ Check that the action can be performed by username on the resource :param action: the name of the permission :param username: the username string or 'anonymous' if there's no authenticated user :param resource: the resource on which the check applies. Will be `None`, if the check is a global one and not made on a resource in particular :param perm: the permission cache for that username and resource, which can be used for doing secondary checks on other permissions. Care must be taken to avoid recursion. :return: `True` if action is allowed, `False` if action is denied, or `None` if indifferent. If `None` is returned, the next policy in the chain will be used, and so on. """ # run the function associated with this action if action in self.action_map: return self.action_map[action](username, resource, perm) # Return None because we don't care about this action so other # PermissionPolicies can vote on this request. return None # --- helpers -------------------------------------------------------------. def _get_resource_id_with_realm(self, resource, realm): while resource: if resource.realm == realm: break resource = resource.parent if not resource or (resource.realm != realm) or (resource.id is None): return None return resource.id def _get_agilo_ticket(self, resource): resource_id = self._get_resource_id_with_realm(resource, Realm.TICKET) return self.tm.get(tkt_id=resource_id) def _get_backlog_name(self, resource): return self._get_resource_id_with_realm(resource, Realm.BACKLOG) def _get_ticket_and_type(self, resource=None, ticket_type=None): ticket = LazyProxy(lambda: self._get_agilo_ticket(resource)) if ticket_type is None: if ticket.get_value() is None: return None, None ticket_type = ticket.get_type() if ticket_type is not None: return ticket, ticket_type return None, None def _get_ticket_type(self, resource): ticket = self._get_agilo_ticket(resource) if ticket is not None: return ticket.get_type() return None