launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28035
[Merge] ~cjwatson/launchpad:tighten-bug-permissions into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:tighten-bug-permissions into launchpad:master.
Commit message:
Tighten bug permissions in view of locking
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/414911
Bug permissions have historically been quite lax, conflating the ability to view a bug with the ability to make various changes to it. Now that we support locking metadata changes to bugs, this has become a problem. This commit tightens up the permissions structure: we should now (mostly) consistently require `launchpad.Append` on bugs or bug tasks to add comments or attachments, and `launchpad.Edit` to make other metadata changes.
Similarly, hide editing UI on bug pages if it can't be used, rather than showing unusable edit buttons on locked bugs.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:tighten-bug-permissions into launchpad:master.
diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py
index f495e7b..b67c65f 100644
--- a/lib/lp/bugs/browser/bug.py
+++ b/lib/lp/bugs/browser/bug.py
@@ -115,6 +115,7 @@ from lp.services.searchbuilder import (
from lp.services.webapp import (
canonical_url,
ContextMenu,
+ enabled_with_permission,
LaunchpadView,
Link,
Navigation,
@@ -225,25 +226,30 @@ class BugContextMenu(ContextMenu):
ContextMenu.__init__(self, getUtility(ILaunchBag).bugtask)
@cachedproperty
+ @enabled_with_permission('launchpad.Edit')
def editdescription(self):
"""Return the 'Edit description/tags' Link."""
text = 'Update description / tags'
return Link('+edit', text, icon='edit')
+ @enabled_with_permission('launchpad.Edit')
def visibility(self):
"""Return the 'Set privacy/security' Link."""
text = 'Change privacy/security'
return Link('+secrecy', text)
+ @enabled_with_permission('launchpad.Edit')
def markduplicate(self):
"""Return the 'Mark as duplicate' Link."""
return Link('+duplicate', 'Mark as duplicate')
+ @enabled_with_permission('launchpad.Edit')
def addupstream(self):
- """Return the 'lso affects project' Link."""
+ """Return the 'Also affects project' Link."""
text = 'Also affects project'
return Link('+choose-affected-product', text, icon='add')
+ @enabled_with_permission('launchpad.Edit')
def adddistro(self):
"""Return the 'Also affects distribution/package' Link."""
text = 'Also affects distribution/package'
@@ -318,16 +324,19 @@ class BugContextMenu(ContextMenu):
else:
return Link('+nominate', '', enabled=False, icon='milestone')
+ @enabled_with_permission('launchpad.Append')
def addcomment(self):
"""Return the 'Comment or attach file' Link."""
text = 'Add attachment or patch'
return Link('+addcomment', text, icon='add')
+ @enabled_with_permission('launchpad.Edit')
def addbranch(self):
"""Return the 'Add branch' Link."""
text = 'Link a related branch'
return Link('+addbranch', text, icon='add')
+ @enabled_with_permission('launchpad.Edit')
def linktocve(self):
"""Return the 'Link to CVE' Link."""
text = structured(
@@ -337,6 +346,7 @@ class BugContextMenu(ContextMenu):
'</abbr>')
return Link('+linkcve', text, icon='add')
+ @enabled_with_permission('launchpad.Edit')
def unlinkcve(self):
"""Return 'Remove CVE link' Link."""
enabled = self.context.bug.has_cves
@@ -347,12 +357,14 @@ class BugContextMenu(ContextMenu):
def _bug_question(self):
return self.context.bug.getQuestionCreatedFromBug()
+ @enabled_with_permission('launchpad.Edit')
def createquestion(self):
"""Create a question from this bug."""
text = 'Convert to a question'
enabled = self._bug_question is None
return Link('+create-question', text, enabled=enabled, icon='add')
+ @enabled_with_permission('launchpad.Edit')
def removequestion(self):
"""Remove the created question from this bug."""
text = 'Convert back to a bug'
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 76e1bb8..ff7d305 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -1705,10 +1705,17 @@ class BugTasksNominationsView(LaunchpadView):
"""Ensure we always have a bug context."""
LaunchpadView.__init__(self, IBug(context), request)
+ def displayAffectedUsers(self):
+ """Return True if UI related to affected users should be displayed."""
+ # Hide this UI when the bug is viewed in a CVE context.
+ return self.request.getNearest(ICveSet) == (None, None)
+
def displayAlsoAffectsLinks(self):
"""Return True if the Also Affects links should be displayed."""
- # Hide the links when the bug is viewed in a CVE context
- return self.request.getNearest(ICveSet) == (None, None)
+ return (
+ check_permission('launchpad.Edit', self.context) and
+ # Hide the links when the bug is viewed in a CVE context.
+ self.request.getNearest(ICveSet) == (None, None))
@cachedproperty
def current_user_affected_status(self):
@@ -2065,7 +2072,6 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
task_link = edit_link = canonical_url(
self.context, view_name='+editstatus')
delete_link = canonical_url(self.context, view_name='+delete')
- can_edit = check_permission('launchpad.Edit', self.context)
bugtask_id = self.context.id
launchbag = getUtility(ILaunchBag)
is_primary = self.context.id == launchbag.bugtask.id
@@ -2073,12 +2079,15 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
# Looking at many_bugtasks is an important optimization. With
# 150+ bugtasks, it can save three or four seconds of rendering
# time.
- expandable=(not self.many_bugtasks and self.canSeeTaskDetails()),
+ expandable=(
+ not self.many_bugtasks and
+ self.user_can_edit and
+ self.canSeeTaskDetails()),
indent_task=ISeriesBugTarget.providedBy(self.context.target),
is_conjoined_replica=self.is_conjoined_replica,
task_link=task_link,
edit_link=edit_link,
- can_edit=can_edit,
+ can_edit=self.user_can_edit,
link=link,
id=bugtask_id,
row_id='tasksummary%d' % bugtask_id,
@@ -2145,8 +2154,10 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
def displayEditForm(self):
"""Return true if the BugTask edit form should be shown."""
- # Hide the edit form when the bug is viewed in a CVE context
- return self.request.getNearest(ICveSet) == (None, None)
+ return (
+ check_permission('launchpad.Edit', self.context) and
+ # Hide the edit form when the bug is viewed in a CVE context.
+ self.request.getNearest(ICveSet) == (None, None))
@property
def status_widget_items(self):
@@ -2224,12 +2235,20 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
return canonical_url(self.context)
@cachedproperty
+ def user_can_edit(self):
+ """Can the user edit the task at all?"""
+ return check_permission('launchpad.Edit', self.context)
+
+ @cachedproperty
def user_can_edit_importance(self):
"""Can the user edit the Importance field?
If yes, return True, otherwise return False.
"""
- return self.user_can_edit_status and self.user_has_privileges
+ return (
+ self.user_can_edit and
+ self.user_can_edit_status and
+ self.user_has_privileges)
@cachedproperty
def user_can_edit_status(self):
@@ -2237,6 +2256,8 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
If yes, return True, otherwise return False.
"""
+ if not self.user_can_edit:
+ return False
bugtask = self.context
edit_allowed = bugtask.pillar.official_malone or bugtask.bugwatch
if bugtask.bugwatch:
@@ -2251,7 +2272,7 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
If yes, return True, otherwise return False.
"""
- return self.user is not None
+ return self.user is not None and self.user_can_edit
@cachedproperty
def user_can_delete_bugtask(self):
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index d9c36a8..85e1340 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -522,7 +522,7 @@
name="+duplicate"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bug.BugMarkAsDuplicateView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/bug-mark-as-duplicate.pt"/>
<browser:page
for="lp.bugs.interfaces.bugtask.IBugTask"
@@ -540,13 +540,13 @@
name="+addcomment"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bugmessage.BugMessageAddFormView"
- permission="launchpad.Edit"
+ permission="launchpad.Append"
template="../templates/bug-comment-add.pt"/>
<browser:page
name="+addcomment-form"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bugmessage.BugMessageAddFormView"
- permission="launchpad.Edit"
+ permission="launchpad.Append"
template="../templates/bug-comment-add-form.pt"/>
<browser:page
name="+nominate"
@@ -558,7 +558,7 @@
name="+addbranch"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bugbranch.BugBranchAddView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/bug-branch-add.pt"/>
<browser:page
name="+index"
@@ -576,18 +576,18 @@
name="+choose-affected-product"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bugalsoaffects.BugAlsoAffectsProductMetaView"
- permission="launchpad.AnyPerson"/>
+ permission="launchpad.Edit"/>
<browser:page
name="+affects-new-product"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bugalsoaffects.BugAlsoAffectsProductWithProductCreationView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/bugtask-affects-new-product.pt"/>
<browser:page
name="+distrotask"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.bugalsoaffects.BugAlsoAffectsDistroMetaView"
- permission="launchpad.AnyPerson"/>
+ permission="launchpad.Edit"/>
<browser:page
name="+editstatus"
for="lp.bugs.interfaces.bugtask.IBugTask"
@@ -640,14 +640,14 @@
name="+linkcve"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.cve.CveLinkView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/bug-cve.pt">
</browser:page>
<browser:page
name="+unlinkcve"
for="lp.bugs.interfaces.bugtask.IBugTask"
class="lp.bugs.browser.cve.CveUnlinkView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/bug-removecve.pt">
</browser:page>
<browser:page
diff --git a/lib/lp/bugs/browser/tests/test_bug_views.py b/lib/lp/bugs/browser/tests/test_bug_views.py
index 9188dfd..b82ae92 100644
--- a/lib/lp/bugs/browser/tests/test_bug_views.py
+++ b/lib/lp/bugs/browser/tests/test_bug_views.py
@@ -24,11 +24,13 @@ from testtools.matchers import (
Not,
)
from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
from lp.app.interfaces.services import IService
from lp.bugs.adapters.bugchange import BugAttachmentChange
+from lp.bugs.enums import BugLockStatus
from lp.registry.enums import BugSharingPolicy
from lp.registry.interfaces.accesspolicy import (
IAccessPolicyGrantSource,
@@ -560,6 +562,24 @@ class TestBugSecrecyViews(TestCaseWithFactory):
contents = view.render()
self.assertTrue(bug.title in contents)
+ def test_locked_target_owner(self):
+ # The target owner can change privacy of locked bugs.
+ bug = self.factory.makeBug()
+ target_owner = bug.default_bugtask.target.owner
+ with person_logged_in(target_owner):
+ bug.lock(who=target_owner, status=BugLockStatus.COMMENT_ONLY)
+ self.createInitializedSecrecyView(person=target_owner, bug=bug)
+ self.assertEqual(InformationType.USERDATA, bug.information_type)
+
+ def test_locked_unprivileged(self):
+ # Unprivileged users cannot change privacy of locked bugs.
+ bug = self.factory.makeBug()
+ target_owner = bug.default_bugtask.target.owner
+ with person_logged_in(target_owner):
+ bug.lock(who=target_owner, status=BugLockStatus.COMMENT_ONLY)
+ self.assertRaises(
+ Unauthorized, self.createInitializedSecrecyView, bug=bug)
+
class TestBugTextViewPrivateTeams(TestCaseWithFactory):
""" Test for rendering BugTextView with private team artifacts.
@@ -787,6 +807,36 @@ class TestBugMarkAsDuplicateView(TestCaseWithFactory):
{'id': 'affected-software', 'class': 'listing'})
self.assertIsNotNone(table)
+ def test_locked_target_owner(self):
+ # The target owner can set a locked bug as a duplicate.
+ target_owner = self.bug.default_bugtask.target.owner
+ with person_logged_in(target_owner):
+ self.bug.lock(who=target_owner, status=BugLockStatus.COMMENT_ONLY)
+ form = {
+ "field.actions.change": "Set Duplicate",
+ "field.duplicateof": str(self.duplicate_bug.id),
+ }
+ create_initialized_view(
+ self.bug.default_bugtask, name="+duplicate",
+ principal=target_owner, form=form)
+ self.assertEqual(self.duplicate_bug, self.bug.duplicateof)
+
+ def test_locked_unprivileged(self):
+ # Unprivileged users (even the bug reporter) cannot set a locked bug
+ # as a duplicate.
+ target_owner = self.bug.default_bugtask.target.owner
+ with person_logged_in(target_owner):
+ self.bug.lock(who=target_owner, status=BugLockStatus.COMMENT_ONLY)
+ with person_logged_in(self.bug_owner):
+ form = {
+ "field.actions.change": "Set Duplicate",
+ "field.duplicateof": str(self.duplicate_bug.id),
+ }
+ self.assertRaises(
+ Unauthorized, create_initialized_view,
+ self.bug.default_bugtask, name="+duplicate",
+ principal=self.bug_owner, form=form)
+
class TestBugActivityView(TestCaseWithFactory):
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 33f6685..3064699 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -735,6 +735,7 @@
unlinkBranch"/>
<require
permission="launchpad.Edit"
+ interface="lp.bugs.interfaces.bug.IBugEdit"
set_attributes="
name
tags
diff --git a/lib/lp/bugs/doc/bug.txt b/lib/lp/bugs/doc/bug.txt
index 34e8de7..028e325 100644
--- a/lib/lp/bugs/doc/bug.txt
+++ b/lib/lp/bugs/doc/bug.txt
@@ -183,7 +183,7 @@ private. A bug cannot be made private by an anonymous user.
Traceback (most recent call last):
...
zope.security.interfaces.Unauthorized:
- (..., 'setPrivate', 'launchpad.Append')
+ (..., 'setPrivate', 'launchpad.Edit')
We have to be logged in, so let's do that:
diff --git a/lib/lp/bugs/interfaces/bug.py b/lib/lp/bugs/interfaces/bug.py
index 7ffc619..7459bd5 100644
--- a/lib/lp/bugs/interfaces/bug.py
+++ b/lib/lp/bugs/interfaces/bug.py
@@ -11,6 +11,7 @@ __all__ = [
'IBugAppend',
'IBugBecameQuestionEvent',
'IBugDelta',
+ 'IBugEdit',
'IBugMute',
'IBugPublic',
'IBugSet',
@@ -726,20 +727,6 @@ class IBugAppend(Interface):
:param update_heat: Whether to update the bug heat.
"""
- @operation_parameters(
- target=Reference(schema=Interface, title=_('Target')))
- @call_with(owner=REQUEST_USER)
- @export_factory_operation(Interface, [])
- def addNomination(owner, target):
- """Nominate a bug for an IDistroSeries or IProductSeries.
-
- :owner: An IPerson.
- :target: An IDistroSeries or IProductSeries.
-
- This method creates and returns a BugNomination. (See
- lp.bugs.model.bugnomination.BugNomination.)
- """
-
@call_with(owner=REQUEST_USER)
@rename_parameters_as(
bugtracker='bug_tracker', remotebug='remote_bug')
@@ -753,33 +740,6 @@ class IBugAppend(Interface):
def removeWatch(bug_watch, owner):
"""Remove a bug watch from the bug."""
- @call_with(owner=REQUEST_USER)
- @operation_parameters(target=copy_field(IBugTask['target']))
- @export_factory_operation(IBugTask, [])
- def addTask(owner, target):
- """Create a new bug task on this bug.
-
- :raises IllegalTarget: if the bug task cannot be added to the bug.
- """
-
- def convertToQuestion(person, comment=None):
- """Create and return a Question from this Bug.
-
- Bugs that are also in external bug trackers cannot be converted
- to questions. This is also true for bugs that are being developed.
-
- The `IQuestionTarget` is provided by the `IBugTask` that is not
- Invalid and is not a conjoined replica. Only one question can be
- made from a bug.
-
- An AssertionError is raised if the bug has zero or many BugTasks
- that can provide a QuestionTarget. It will also be raised if a
- question was previously created from the bug.
-
- :person: The `IPerson` creating a question from this bug
- :comment: A string. An explanation of why the bug is a question.
- """
-
def expireNotifications():
"""Expire any pending notifications that have not been emailed.
@@ -811,81 +771,6 @@ class IBugAppend(Interface):
file_alias.restricted.
"""
- @call_with(user=REQUEST_USER)
- @operation_parameters(
- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
- merge_proposal=Reference(
- Interface, title=_('Merge proposal'), required=True))
- @export_write_operation()
- @operation_for_version('devel')
- def linkMergeProposal(merge_proposal, user, check_permissions=True):
- """Ensure that this MP is linked to this bug."""
-
- @call_with(user=REQUEST_USER)
- @operation_parameters(
- # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
- merge_proposal=Reference(
- Interface, title=_('Merge proposal'), required=True))
- @export_write_operation()
- @operation_for_version('devel')
- def unlinkMergeProposal(merge_proposal, user, check_permissions=True):
- """Ensure that any links between this bug and the given MP are removed.
- """
-
- @call_with(user=REQUEST_USER)
- @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
- @export_write_operation()
- def linkCVE(cve, user, check_permissions=True):
- """Ensure that this CVE is linked to this bug."""
-
- @call_with(user=REQUEST_USER)
- @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
- @export_write_operation()
- def unlinkCVE(cve, user, check_permissions=True):
- """Ensure that any links between this bug and the given CVE are
- removed.
- """
-
- @mutator_for(IBugPublic['private'])
- @operation_parameters(private=copy_field(IBugPublic['private']))
- @call_with(who=REQUEST_USER)
- @export_write_operation()
- def setPrivate(private, who):
- """Set bug privacy.
-
- :private: True/False.
- :who: The IPerson who is making the change.
-
- Return True if a change is made, False otherwise.
- """
-
- @mutator_for(IBugView['security_related'])
- @operation_parameters(
- security_related=copy_field(IBugView['security_related']))
- @call_with(who=REQUEST_USER)
- @export_write_operation()
- def setSecurityRelated(security_related, who):
- """Set bug security.
-
- :security_related: True/False.
- :who: The IPerson who is making the change.
-
- Return True if a change is made, False otherwise.
- """
-
- @operation_parameters(
- information_type=copy_field(IBugPublic['information_type']),
- )
- @call_with(who=REQUEST_USER)
- @export_write_operation()
- @operation_for_version("devel")
- def transitionToInformationType(information_type, who):
- """Set the information type for this bug.
-
- :information_type: The `InformationType` to transition to.
- :who: The `IPerson` who is making the change.
- """
-
def linkMessage(message, bugwatch=None, user=None,
remote_comment_id=None):
"""Add a comment to this bug.
@@ -907,12 +792,6 @@ class IBugAppend(Interface):
def markUserAffected(user, affected=True):
"""Mark :user: as affected by this bug."""
- @mutator_for(IBugView['duplicateof'])
- @operation_parameters(duplicate_of=copy_field(IBugView['duplicateof']))
- @export_write_operation()
- def markAsDuplicate(duplicate_of):
- """Mark this bug as a duplicate of another."""
-
@operation_parameters(
comment_number=Int(
title=_('The number of the comment in the list of messages.'),
@@ -983,6 +862,132 @@ class IBugAppend(Interface):
def unsubscribeFromDupes(person, unsubscribed_by):
"""Remove this person's subscription from all dupes of this bug."""
+
+class IBugEdit(Interface):
+ """IBug attributes that require launchpad.Edit permission."""
+
+ @operation_parameters(
+ target=Reference(schema=Interface, title=_('Target')))
+ @call_with(owner=REQUEST_USER)
+ @export_factory_operation(Interface, [])
+ def addNomination(owner, target):
+ """Nominate a bug for an IDistroSeries or IProductSeries.
+
+ :owner: An IPerson.
+ :target: An IDistroSeries or IProductSeries.
+
+ This method creates and returns a BugNomination. (See
+ lp.bugs.model.bugnomination.BugNomination.)
+ """
+
+ @call_with(owner=REQUEST_USER)
+ @operation_parameters(target=copy_field(IBugTask['target']))
+ @export_factory_operation(IBugTask, [])
+ def addTask(owner, target):
+ """Create a new bug task on this bug.
+
+ :raises IllegalTarget: if the bug task cannot be added to the bug.
+ """
+
+ def convertToQuestion(person, comment=None):
+ """Create and return a Question from this Bug.
+
+ Bugs that are also in external bug trackers cannot be converted
+ to questions. This is also true for bugs that are being developed.
+
+ The `IQuestionTarget` is provided by the `IBugTask` that is not
+ Invalid and is not a conjoined replica. Only one question can be
+ made from a bug.
+
+ An AssertionError is raised if the bug has zero or many BugTasks
+ that can provide a QuestionTarget. It will also be raised if a
+ question was previously created from the bug.
+
+ :person: The `IPerson` creating a question from this bug
+ :comment: A string. An explanation of why the bug is a question.
+ """
+
+ @call_with(user=REQUEST_USER)
+ @operation_parameters(
+ # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
+ merge_proposal=Reference(
+ Interface, title=_('Merge proposal'), required=True))
+ @export_write_operation()
+ @operation_for_version('devel')
+ def linkMergeProposal(merge_proposal, user, check_permissions=True):
+ """Ensure that this MP is linked to this bug."""
+
+ @call_with(user=REQUEST_USER)
+ @operation_parameters(
+ # Really IBranchMergeProposal, patched in _schema_circular_imports.py.
+ merge_proposal=Reference(
+ Interface, title=_('Merge proposal'), required=True))
+ @export_write_operation()
+ @operation_for_version('devel')
+ def unlinkMergeProposal(merge_proposal, user, check_permissions=True):
+ """Ensure that any links between this bug and the given MP are removed.
+ """
+
+ @call_with(user=REQUEST_USER)
+ @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
+ @export_write_operation()
+ def linkCVE(cve, user, check_permissions=True):
+ """Ensure that this CVE is linked to this bug."""
+
+ @call_with(user=REQUEST_USER)
+ @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
+ @export_write_operation()
+ def unlinkCVE(cve, user, check_permissions=True):
+ """Ensure that any links between this bug and the given CVE are
+ removed.
+ """
+
+ @mutator_for(IBugPublic['private'])
+ @operation_parameters(private=copy_field(IBugPublic['private']))
+ @call_with(who=REQUEST_USER)
+ @export_write_operation()
+ def setPrivate(private, who):
+ """Set bug privacy.
+
+ :private: True/False.
+ :who: The IPerson who is making the change.
+
+ Return True if a change is made, False otherwise.
+ """
+
+ @mutator_for(IBugView['security_related'])
+ @operation_parameters(
+ security_related=copy_field(IBugView['security_related']))
+ @call_with(who=REQUEST_USER)
+ @export_write_operation()
+ def setSecurityRelated(security_related, who):
+ """Set bug security.
+
+ :security_related: True/False.
+ :who: The IPerson who is making the change.
+
+ Return True if a change is made, False otherwise.
+ """
+
+ @operation_parameters(
+ information_type=copy_field(IBugPublic['information_type']),
+ )
+ @call_with(who=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def transitionToInformationType(information_type, who):
+ """Set the information type for this bug.
+
+ :information_type: The `InformationType` to transition to.
+ :who: The `IPerson` who is making the change.
+ """
+
+ @mutator_for(IBugView['duplicateof'])
+ @operation_parameters(duplicate_of=copy_field(IBugView['duplicateof']))
+ @export_write_operation()
+ def markAsDuplicate(duplicate_of):
+ """Mark this bug as a duplicate of another."""
+
def setStatus(target, status, user):
"""Set the status of the bugtask related to the specified target.
@@ -997,6 +1002,7 @@ class IBugAppend(Interface):
Return None if no bugtask was edited.
"""
+
class IBugModerate(Interface):
"""IBug attributes that require the launchpad.Moderate permission."""
@@ -1044,7 +1050,8 @@ class IBugModerate(Interface):
@exported_as_webservice_entry()
-class IBug(IBugPublic, IBugView, IBugAppend, IBugModerate, IHasLinkedBranches):
+class IBug(IBugPublic, IBugView, IBugAppend, IBugEdit, IBugModerate,
+ IHasLinkedBranches):
"""The core bug entry."""
linked_bugbranches = exported(
diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py
index 9ec2b05..57ad9fa 100644
--- a/lib/lp/bugs/security.py
+++ b/lib/lp/bugs/security.py
@@ -38,6 +38,19 @@ class EditBugNominationStatus(AuthorizationBase):
return self.obj.canApprove(user.person)
+class AppendBugTask(DelegatedAuthorization):
+ """Security adapter for appending to bug tasks.
+
+ This has the same semantics as `AppendBug`, but can be used where the
+ context is a bug task rather than a bug.
+ """
+ permission = 'launchpad.Append'
+ usedfor = IHasBug
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.bug)
+
+
class EditBugTask(DelegatedAuthorization):
"""Permission checker for editing objects linked to a bug.
diff --git a/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt b/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
index 67bf508..d58193d 100644
--- a/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
+++ b/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
@@ -716,7 +716,7 @@ logged in.
--> None
>>> print_assigned_bugtasks(anon_browser)
- GNOME Terminal ... -->
+ GNOME Terminal -->
auto-dark-master-o-bugs
>>> anon_browser.contents.count(
diff --git a/lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt b/lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt
index 52c7c53..ffbf5d9 100644
--- a/lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt
+++ b/lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt
@@ -5,8 +5,13 @@ Anonymous users should not be able to nominate bugs for release because
launchpad.Edit permission is required to do so.:
>>> anon_browser.open('http://bugs.launchpad.test/jokosher/+bug/12')
+ >>> anon_browser.getLink('Nominate for series')
+ Traceback (most recent call last):
+ ...
+ zope.testbrowser.browser.LinkNotFoundError
- >>> anon_browser.getLink('Nominate for series').click()
+ >>> anon_browser.open(
+ ... 'http://bugs.launchpad.test/jokosher/+bug/12/+nominate')
Traceback (most recent call last):
...
zope.security.interfaces.Unauthorized: ...'launchpad.Edit'...
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt b/lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt
index a870168..3494cc4 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt
@@ -1,14 +1,14 @@
Bug commenting and attaching files is done on the same form.
When a comment is submitted, the attachment is optional. Accessing the
-comment form requires launchpad.Edit permission on the bugtask. In this
+comment form requires launchpad.Append permission on the bugtask. In this
case, it means being logged in.
>>> anon_browser.open(
... "http://bugs.launchpad.test/firefox/+bug/1/+addcomment-form")
Traceback (most recent call last):
..
- zope.security.interfaces.Unauthorized: ...launchpad.Edit...
+ zope.security.interfaces.Unauthorized: ...launchpad.Append...
So let's login and add a comment.
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-create-question.txt b/lib/lp/bugs/stories/bugs/xx-bug-create-question.txt
index b2d2ee3..2b65420 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-create-question.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-create-question.txt
@@ -16,7 +16,14 @@ An anonymous user cannot create a question.
>>> anon_browser.title
'Bug #10 ... : Bugs : linux-source-2.6.15 package : Ubuntu'
- >>> anon_browser.getLink('Convert to a question').click()
+ >>> anon_browser.getLink('Convert to a question')
+ Traceback (most recent call last):
+ ...
+ zope.testbrowser.browser.LinkNotFoundError
+
+ >>> anon_browser.open(
+ ... 'http://bugs.launchpad.test'
+ ... '/ubuntu/+source/linux-source-2.6.15/+bug/10/+create-question')
Traceback (most recent call last):
...
zope.security.interfaces.Unauthorized: ...
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-edit.txt b/lib/lp/bugs/stories/bugs/xx-bug-edit.txt
index 02195c1..7700e4a 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-edit.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-edit.txt
@@ -94,3 +94,37 @@ Now, let's add 'new-tag' again, and confirm it this time.
False
>>> 'new-tag' in tags_div
True
+
+
+Locked bugs
+-----------
+
+The metadata of a locked bug can only be edited by privileged users.
+
+ >>> from zope.component import getUtility
+ >>> from lp.bugs.enums import BugLockStatus
+ >>> from lp.bugs.interfaces.bug import IBugSet
+ >>> from lp.testing.pages import setupBrowserForUser
+
+ >>> login(ANONYMOUS)
+ >>> bug_1 = getUtility(IBugSet).get(1)
+ >>> target_owner = bug_1.default_bugtask.target.owner
+ >>> _ = login_person(target_owner)
+ >>> bug_1.lock(who=target_owner, status=BugLockStatus.COMMENT_ONLY)
+ >>> logout()
+
+ >>> target_owner_browser = setupBrowserForUser(target_owner)
+ >>> target_owner_browser.open('http://bugs.launchpad.test/firefox/+bug/1')
+ >>> target_owner_browser.getLink(url='+edit').click()
+ >>> target_owner_browser.getControl('Description').value = 'Now locked.'
+ >>> target_owner_browser.getControl('Change').click()
+
+ >>> user_browser.open('http://bugs.launchpad.test/firefox/+bug/1')
+ >>> user_browser.getLink(url='+edit')
+ Traceback (most recent call last):
+ ...
+ zope.testbrowser.browser.LinkNotFoundError
+ >>> user_browser.open('http://bugs.launchpad.test/firefox/+bug/1/+edit')
+ Traceback (most recent call last):
+ ...
+ zope.security.interfaces.Unauthorized: ...
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-index.txt b/lib/lp/bugs/stories/bugs/xx-bug-index.txt
index 3af7922..590df67 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-index.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-index.txt
@@ -154,9 +154,9 @@ initialization code if there's more than 10 bug tasks.
On the bug page, for every bug task there's one expander.
- >>> browser.open(bug_url)
+ >>> user_browser.open(bug_url)
>>> print(len(find_tags_by_class(
- ... browser.contents, 'bugtask-expander')))
+ ... user_browser.contents, 'bugtask-expander')))
1
We add four more tasks.
@@ -168,9 +168,9 @@ We add four more tasks.
And the expander appears five times.
- >>> browser.open(bug_url)
+ >>> user_browser.open(bug_url)
>>> print(len(find_tags_by_class(
- ... browser.contents, 'bugtask-expander')))
+ ... user_browser.contents, 'bugtask-expander')))
5
But on pages with more than ten bug tasks, we don't include the expander
@@ -181,8 +181,49 @@ at all.
... _ = bug.addTask(bug.owner, factory.makeProduct())
>>> logout()
- >>> browser.open(bug_url)
+ >>> user_browser.open(bug_url)
>>> print(len(find_tags_by_class(
- ... browser.contents, 'bugtask-expander')))
+ ... user_browser.contents, 'bugtask-expander')))
0
+We also don't include the expander for anonymous requests.
+
+ >>> anon_browser.open(bug_url)
+ >>> print(len(find_tags_by_class(
+ ... anon_browser.contents, 'bugtask-expander')))
+ 0
+
+
+Locked bugs
+-----------
+
+Unprivileged users viewing locked bugs don't see the bugtask expander, nor
+do they see any edit links.
+
+ >>> import re
+ >>> from lp.bugs.enums import BugLockStatus
+ >>> from lp.testing.pages import setupBrowserForUser
+
+ >>> login('foo.bar@xxxxxxxxxxxxx')
+ >>> locked_bug = factory.makeBug()
+ >>> target_owner = locked_bug.default_bugtask.target.owner
+ >>> locked_bug.lock(who=target_owner, status=BugLockStatus.COMMENT_ONLY)
+ >>> locked_bug_url = canonical_url(locked_bug)
+ >>> logout()
+
+ >>> target_owner_browser = setupBrowserForUser(target_owner)
+ >>> target_owner_browser.open(locked_bug_url)
+ >>> print(len(find_tags_by_class(
+ ... target_owner_browser.contents, 'bugtask-expander')))
+ 1
+ >>> len(find_main_content(target_owner_browser.contents).find_all(
+ ... 'a', href=re.compile(r'.*/\+edit.*')))
+ 5
+
+ >>> user_browser.open(locked_bug_url)
+ >>> print(len(find_tags_by_class(
+ ... user_browser.contents, 'bugtask-expander')))
+ 0
+ >>> len(find_main_content(user_browser.contents).find_all(
+ ... 'a', href=re.compile(r'.*/\+edit.*')))
+ 0
diff --git a/lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt b/lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt
index c906387..10c906c 100644
--- a/lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt
+++ b/lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt
@@ -32,3 +32,24 @@ Any logged-in user can edit a bugtask.
>>> browser.open("http://launchpad.test/firefox/+bug/6/+editstatus")
>>> print(browser.title)
Edit status ...
+
+But only privileged users can edit bugtasks on locked bugs.
+
+ >>> from zope.component import getUtility
+ >>> from lp.bugs.enums import BugLockStatus
+ >>> from lp.bugs.interfaces.bug import IBugSet
+
+ >>> login("test@xxxxxxxxxxxxx")
+ >>> getUtility(IBugSet).get(6).lock(
+ ... who=sample_person, status=BugLockStatus.COMMENT_ONLY)
+ >>> logout()
+
+ >>> browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
+ >>> browser.open("http://launchpad.test/firefox/+bug/6/+editstatus")
+ >>> print(browser.title)
+ Edit status ...
+
+ >>> user_browser.open("http://launchpad.test/firefox/+bug/6/+editstatus")
+ Traceback (most recent call last):
+ ...
+ zope.security.interfaces.Unauthorized: ...
diff --git a/lib/lp/bugs/templates/bugtask-index.pt b/lib/lp/bugs/templates/bugtask-index.pt
index 911161b..287bb85 100644
--- a/lib/lp/bugs/templates/bugtask-index.pt
+++ b/lib/lp/bugs/templates/bugtask-index.pt
@@ -179,14 +179,16 @@
class="unofficial-tag"
tal:attributes="href python: tag[1]">tag</a>
</span>
- <a href="+edit" title="Add tags" id="tags-trigger"
- class="sprite add"
- tal:condition="not: context/bug/tags">Add tags</a>
- <a href="+edit" title="Edit tags" id="tags-trigger"
- class="sprite edit action-icon"
- tal:condition="context/bug/tags">Edit</a>
- <a href="/+help-bugs/tag-help.html" target="help"
- class="sprite maybe action-icon">Tag help</a>
+ <tal:can-edit condition="context/required:launchpad.Edit">
+ <a href="+edit" title="Add tags" id="tags-trigger"
+ class="sprite add"
+ tal:condition="not: context/bug/tags">Add tags</a>
+ <a href="+edit" title="Edit tags" id="tags-trigger"
+ class="sprite edit action-icon"
+ tal:condition="context/bug/tags">Edit</a>
+ <a href="/+help-bugs/tag-help.html" target="help"
+ class="sprite maybe action-icon">Tag help</a>
+ </tal:can-edit>
</div>
<script type="text/javascript">
diff --git a/lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt b/lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt
index 4992385..8702dd7 100644
--- a/lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt
+++ b/lib/lp/bugs/templates/bugtask-tasks-and-nominations-table-row.pt
@@ -33,7 +33,8 @@
class context/target/image:sprite_css"
tal:content="context/bugtargetdisplayname" />
</span>
- <button class="lazr-btn yui3-activator-act yui3-activator-hidden">
+ <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
+ tal:condition="data/can_edit">
Edit
</button>
<div class="yui3-activator-message-box yui3-activator-hidden"></div>
@@ -152,7 +153,8 @@
tal:attributes="href data/edit_link">
<img class="editicon" src="/@@/edit" />
</a>
- <button class="lazr-btn yui3-activator-act yui3-activator-hidden">
+ <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
+ tal:condition="view/user_can_edit_assignee">
Edit
</button>
<div class="yui3-activator-message-box yui3-activator-hidden"></div>
diff --git a/lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt b/lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt
index 3f53c63..25d02d9 100644
--- a/lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt
+++ b/lib/lp/bugs/templates/bugtasks-and-nominations-portal.pt
@@ -4,7 +4,7 @@
omit-tag="">
<tal:affects-me-too
- tal:condition="view/displayAlsoAffectsLinks">
+ tal:condition="view/displayAffectedUsers">
<tal:editable
condition="context/menu:context/affectsmetoo/enabled">
<div class="actions">