← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/rollback-r15645 into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/rollback-r15645 into lp:launchpad.

Requested reviews:
  Steve Kowalik (stevenk): code

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/rollback-r15645/+merge/115647

Rollback r15645. It's broken. :-(
-- 
https://code.launchpad.net/~stevenk/launchpad/rollback-r15645/+merge/115647
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2012-07-18 13:47:23 +0000
+++ database/schema/security.cfg	2012-07-19 04:54:38 +0000
@@ -1512,10 +1512,8 @@
 [bugnotification]
 groups=script
 public.accessartifact                   = SELECT, INSERT, DELETE
-public.accessartifactgrant              = SELECT
 public.accesspolicy                     = SELECT
 public.accesspolicyartifact             = SELECT, INSERT, DELETE
-public.accesspolicygrant                = SELECT
 public.account                          = SELECT
 public.answercontact                    = SELECT
 public.archive                          = SELECT

=== modified file 'lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt'
--- lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt	2012-07-17 22:37:49 +0000
+++ lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt	2012-07-19 04:54:38 +0000
@@ -387,6 +387,17 @@
     >>> filebug_view.added_bug.information_type
     <DBItem InformationType.USERDATA, (4) Private>
 
+Since the bug was marked private before it was filed, only the bug
+reporter has been subscribed to the bug and there should be no indirect
+subscribers.
+
+    >>> for subscriber in filebug_view.added_bug.getDirectSubscribers():
+    ...     print subscriber.displayname
+    No Privileges Person
+
+    >>> filebug_view.added_bug.getIndirectSubscribers()
+    []
+
 The user will be notified that the bug has been marked as private.
 
     >>> print filebug_view.request.response.notifications[2].message
@@ -584,6 +595,11 @@
     Mark Shuttleworth
     No Privileges Person
 
+Since the bug is private, there should be no indirect subscribers.
+
+    >>> filebug_view.added_bug.getIndirectSubscribers()
+    []
+
 The user will be notified that Mark Shuttleworth has been subscribed to
 this bug and that the bug has been marked as private.
 

=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt	2012-07-17 06:21:47 +0000
+++ lib/lp/bugs/doc/bug.txt	2012-07-19 04:54:38 +0000
@@ -423,6 +423,13 @@
     >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()]
     [u'name16']
 
+Since it's private, there are no indirect subscribers.
+
+    >>> private_bug.getIndirectSubscribers()
+    []
+
+It's up to the submitter to subscribe the maintainer, if they so choose.
+
 This works similarly for distributions; in this case the
 "maintainer" is considered the person who maintains the applicable
 sourcepackage. E.g.
@@ -442,6 +449,8 @@
     >>> private_bug = bugset.get(added_bug.id)
     >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()]
     [u'name16']
+    >>> private_bug.getIndirectSubscribers()
+    []
 
 
 Prevent reporter from being subscribed to filed bugs

=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt	2012-07-17 22:19:31 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt	2012-07-19 04:54:38 +0000
@@ -405,6 +405,18 @@
     Mark Shuttleworth
     Robert Collins
 
+A private bug never has indirect subscribers. Let's add an indirect subscriber
+to show that they still aren't included in the indirect subscriptions.
+
+    >>> linux_source_bug.bugtasks[0].transitionToAssignee(
+    ...     personset.getByName("martin-pitt"))
+
+    >>> linux_source_bug.getIndirectSubscribers()
+    []
+
+    >>> linux_source_bug.getSubscribersFromDuplicates()
+    ()
+
 Direct subscriptions always take precedence over indirect subscriptions.
 So, if we unmark the above bug as private, indirect_subscribers will include
 any subscribers not converted to direct subscribers.
@@ -418,12 +430,14 @@
     Robert Collins
 
     >>> print_displayname(linux_source_bug.getIndirectSubscribers())
+    Martin Pitt
     No Privileges Person
     Sample Person
     Scott James Remnant
     Ubuntu Team
 
     >>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
+    Martin Pitt
     No Privileges Person
     Sample Person
     Scott James Remnant
@@ -767,6 +781,20 @@
     robertc@xxxxxxxxxxxxxxxxx
     test@xxxxxxxxxxxxx
 
+The upstream maintainer will be subscribed to security-related private
+bugs, because upstream has no security contact, in this case.
+
+    >>> firefox.security_contact is None
+    True
+
+    >>> params = CreateBugParams(
+    ...     title="a test bug", comment="a test description",
+    ...     owner=foobar, information_type=InformationType.PRIVATESECURITY)
+    >>> new_bug = firefox.createBug(params)
+
+    >>> getSubscribers(new_bug)
+    ['foo.bar@xxxxxxxxxxxxx', 'test@xxxxxxxxxxxxx']
+
 Now let's create a bug on a specific package, which has no package bug
 contacts:
 

=== modified file 'lib/lp/bugs/doc/security-teams.txt'
--- lib/lp/bugs/doc/security-teams.txt	2012-07-17 22:19:31 +0000
+++ lib/lp/bugs/doc/security-teams.txt	2012-07-19 04:54:38 +0000
@@ -67,6 +67,9 @@
     ...         bug.getIndirectSubscribers())
     ...     return sorted(subscriber.name for subscriber in subscribers)
 
+    >>> subscriber_names(bug)
+    [u'mark', u'name16']
+
 If the bug were not reported as security-related, only Foo Bar would
 have been subscribed:
 
@@ -101,6 +104,9 @@
     >>> bug.private
     True
 
+    >>> subscriber_names(bug)
+    [u'name16', u'ubuntu-team']
+
 Again, if the bug were not reported as security-related, the security
 contact, the Ubuntu Team, would not have been subscribed:
 

=== modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py'
--- lib/lp/bugs/mail/bugnotificationrecipients.py	2012-07-17 06:21:47 +0000
+++ lib/lp/bugs/mail/bugnotificationrecipients.py	2012-07-19 04:54:38 +0000
@@ -56,7 +56,7 @@
         duplicateof parameter above and the addDupeSubscriber method.
         Don't confuse them!
         """
-        super(BugNotificationRecipients, self).__init__()
+        NotificationRecipientSet.__init__(self)
         self.duplicateof = duplicateof
         self.subscription_filters = set()
 

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2012-07-17 22:19:31 +0000
+++ lib/lp/bugs/model/bug.py	2012-07-19 04:54:38 +0000
@@ -1134,34 +1134,24 @@
                                      level=None,
                                      include_master_dupe_subscribers=False):
         """See `IBug`."""
-        # Circular fail :(
-        from lp.bugs.model.bugtaskflat import BugTaskFlat
-        from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms
-
         recipients = BugNotificationRecipients(duplicateof=duplicateof)
         self.getDirectSubscribers(recipients, level=level)
-        self.getIndirectSubscribers(recipients, level=level)
-
-        # Only include recipients who can see the bug, if it's private.
         if self.private:
-            ids = [person.id for person in recipients]
-            forbidden_recipients = IStore(Person).find(
-                Person,
-                Not(Or(*get_bug_privacy_filter_terms(Person.id))),
-                Person.id.is_in(ids), BugTaskFlat.bug_id == self.id)
-            for recipient in forbidden_recipients:
-                if recipient in recipients:
-                    recipients.remove(recipient)
-
-        if include_master_dupe_subscribers and self.duplicateof:
-            # This bug is a public duplicate of another bug, so include
-            # the dupe target's subscribers in the recipient list. Note
-            # that we only do this for duplicate bugs that are public;
-            # changes in private bugs are not broadcast to their dupe
-            # targets.
-            dupe_recipients = self.duplicateof.getBugNotificationRecipients(
-                duplicateof=self.duplicateof, level=level)
-            recipients.update(dupe_recipients)
+            assert self.getIndirectSubscribers() == [], (
+                "Indirect subscribers found on private bug. "
+                "A private bug should never have implicit subscribers!")
+        else:
+            self.getIndirectSubscribers(recipients, level=level)
+            if include_master_dupe_subscribers and self.duplicateof:
+                # This bug is a public duplicate of another bug, so include
+                # the dupe target's subscribers in the recipient list. Note
+                # that we only do this for duplicate bugs that are public;
+                # changes in private bugs are not broadcast to their dupe
+                # targets.
+                dupe_recipients = (
+                    self.duplicateof.getBugNotificationRecipients(
+                        duplicateof=self.duplicateof, level=level))
+                recipients.update(dupe_recipients)
         # XXX Tom Berger 2008-03-18:
         # We want to look up the recipients for `old_bug` too,
         # but for this to work, this code has to move out of the
@@ -2273,6 +2263,9 @@
     else:
         raise ValueError('First argument must be bug or bugtask')
 
+    if bug.private:
+        return []
+
     # Subscribers to exclude.
     exclude_subscribers = frozenset().union(
         info.direct_subscribers_at_all_levels, info.muted_subscribers)
@@ -2533,16 +2526,19 @@
 
         Excludes muted subscriptions.
         """
-        return IStore(BugSubscription).find(
-            BugSubscription,
-            BugSubscription.bug_notification_level >= self.level,
-            BugSubscription.bug_id == Bug.id,
-            Bug.duplicateof == self.bug,
-            Not(In(
-                BugSubscription.person_id,
-                Select(
-                    BugMute.person_id, BugMute.bug_id == Bug.id,
-                    tables=[BugMute]))))
+        if self.bug.private:
+            return ()
+        else:
+            return IStore(BugSubscription).find(
+                BugSubscription,
+                BugSubscription.bug_notification_level >= self.level,
+                BugSubscription.bug_id == Bug.id,
+                Bug.duplicateof == self.bug,
+                Not(In(
+                    BugSubscription.person_id,
+                    Select(
+                        BugMute.person_id, BugMute.bug_id == Bug.id,
+                        tables=[BugMute]))))
 
     @property
     def duplicate_subscribers(self):
@@ -2630,13 +2626,16 @@
     @cachedproperty
     def also_notified_subscribers(self):
         """All subscribers except direct, dupe, and muted subscribers."""
-        subscribers = BugSubscriberSet().union(
-            self.structural_subscribers,
-            self.all_pillar_owners_without_bug_supervisors,
-            self.all_assignees)
-        return subscribers.difference(
-            self.direct_subscribers_at_all_levels,
-            self.muted_subscribers)
+        if self.bug.private:
+            return BugSubscriberSet()
+        else:
+            subscribers = BugSubscriberSet().union(
+                self.structural_subscribers,
+                self.all_pillar_owners_without_bug_supervisors,
+                self.all_assignees)
+            return subscribers.difference(
+                self.direct_subscribers_at_all_levels,
+                self.muted_subscribers)
 
     @cachedproperty
     def indirect_subscribers(self):

=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py	2012-07-17 22:19:31 +0000
+++ lib/lp/bugs/model/tests/test_bug.py	2012-07-19 04:54:38 +0000
@@ -891,6 +891,25 @@
             bug.setPrivate(True, bug.owner)
         self.assertEqual(InformationType.USERDATA, bug.information_type)
 
+    def test_information_type_does_not_leak(self):
+        # Make sure that bug notifications for private bugs do not leak to
+        # people with a subscription on the product.
+        product = self.factory.makeProduct()
+        with person_logged_in(product.owner):
+            product.addSubscription(product.owner, product.owner)
+        reporter = self.factory.makePerson()
+        bug = self.factory.makeBug(
+            information_type=InformationType.USERDATA, product=product,
+            owner=reporter)
+        recipients = Store.of(bug).using(
+            BugNotificationRecipient,
+            Join(BugNotification, BugNotification.bugID == bug.id)).find(
+            BugNotificationRecipient,
+            BugNotificationRecipient.bug_notificationID ==
+                BugNotification.id)
+        self.assertEqual(
+            [reporter], [recipient.person for recipient in recipients])
+
     def test__reconcileAccess_handles_all_targets(self):
         # _reconcileAccess gets the pillar from any task
         # type.

=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py'
--- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py	2012-07-17 06:21:47 +0000
+++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py	2012-07-19 04:54:38 +0000
@@ -273,6 +273,18 @@
         self.assertContentEqual(
             [duplicate_bug_subscription], found_subscriptions)
 
+    def test_duplicate_for_private_bug(self):
+        # The set of subscribers from duplicate bugs is always empty when the
+        # master bug is private.
+        duplicate_bug = self.factory.makeBug(product=self.target)
+        with person_logged_in(duplicate_bug.owner):
+            duplicate_bug.markAsDuplicate(self.bug)
+        with person_logged_in(self.bug.owner):
+            self.bug.setPrivate(True, self.bug.owner)
+        found_subscriptions = self.getInfo().duplicate_subscriptions
+        self.assertContentEqual([], found_subscriptions)
+        self.assertContentEqual([], found_subscriptions.subscribers)
+
     def test_duplicate_only(self):
         # The set of duplicate subscriptions where the subscriber has no other
         # subscriptions.
@@ -479,6 +491,33 @@
         found_subscribers = self.getInfo().also_notified_subscribers
         self.assertContentEqual([], found_subscribers)
 
+    def test_also_notified_subscribers_for_private_bug(self):
+        # The set of also notified subscribers is always empty when the master
+        # bug is private.
+        assignee = self.factory.makePerson()
+        bugtask = self.bug.default_bugtask
+        with person_logged_in(bugtask.pillar.bug_supervisor):
+            bugtask.transitionToAssignee(assignee)
+        with person_logged_in(self.bug.owner):
+            self.bug.setPrivate(True, self.bug.owner)
+        found_subscribers = self.getInfo().also_notified_subscribers
+        self.assertContentEqual([], found_subscribers)
+
+    def test_indirect_subscribers(self):
+        # The set of indirect subscribers is the union of also notified
+        # subscribers and subscribers to duplicates.
+        assignee = self.factory.makePerson()
+        bugtask = self.bug.default_bugtask
+        with person_logged_in(bugtask.pillar.bug_supervisor):
+            bugtask.transitionToAssignee(assignee)
+        duplicate_bug = self.factory.makeBug(product=self.target)
+        with person_logged_in(duplicate_bug.owner):
+            duplicate_bug.markAsDuplicate(self.bug)
+        found_subscribers = self.getInfo().indirect_subscribers
+        self.assertContentEqual(
+            [assignee, duplicate_bug.owner],
+            found_subscribers)
+
 
 class TestBugSubscriptionInfoPermissions(TestCaseWithFactory):
 
@@ -606,9 +645,9 @@
         self.make_duplicate_bug()
         with person_logged_in(self.bug.owner):
             self.bug.setPrivate(True, self.bug.owner)
-        with self.exactly_x_queries(1):
+        with self.exactly_x_queries(0):
             self.info.duplicate_subscriptions
-        with self.exactly_x_queries(1):
+        with self.exactly_x_queries(0):
             self.info.duplicate_subscriptions.subscribers
 
     def add_structural_subscriber(self):

=== added file 'lib/lp/bugs/stories/initial-bug-contacts/xx-initial-bug-contacts.txt'
--- lib/lp/bugs/stories/initial-bug-contacts/xx-initial-bug-contacts.txt	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/stories/initial-bug-contacts/xx-initial-bug-contacts.txt	2012-07-19 04:54:38 +0000
@@ -0,0 +1,237 @@
+To set a bug supervisor for a distribution, we need to go to the Bug page
+of that distribution:
+
+  >>> browser.open('http://launchpad.dev/ubuntu/+bugs')
+
+But the link is not available if you are not logged in with permission
+to change the bug supervisor.
+
+  >>> browser.getLink("Change bug supervisor")
+  Traceback (most recent call last):
+  ...
+  LinkNotFoundError
+
+Colin is an Ubuntu owner and can set the bug supervisor role.
+
+  >>> colin_browser = setupBrowser(
+  ...     auth='Basic colin.watson@xxxxxxxxxxxxxxx:test')
+  >>> colin_browser.open('http://bugs.launchpad.dev/ubuntu/+bugs')
+
+...and he can see that the link on Ubuntu's bugs page.
+
+  >>> bug_supervisor_link = colin_browser.getLink("Change bug supervisor")
+  >>> bug_supervisor_link.url
+  'http://bugs.launchpad.dev/ubuntu/+bugsupervisor'
+
+Anyone with launchpad.Edit permission can edit the distribution bug
+supervisor, but most users can select only themselves and the teams they
+administer. In this example, Colin will set himself as the distribution
+bug supervisor.
+
+  >>> bug_supervisor_link.click()
+  >>> colin_browser.url
+  'http://bugs.launchpad.dev/ubuntu/+bugsupervisor'
+
+The bug supervisor page takes just one simple value: the bug supervisor email
+or nickname. Let's set colin.watson@xxxxxxxxxxxxxxx as the bug supervisor for
+Ubuntu.
+
+  >>> colin_browser.getControl("Bug Supervisor").value = (
+  ...     ' colin.watson@xxxxxxxxxxxxxxx ')
+  >>> colin_browser.getControl("Change").click()
+
+And then Colin is redirected to the distribution bugs page.
+
+    >>> print extract_text(find_tag_by_id(
+    ...     colin_browser.contents, 'bug-supervisor'))
+    Bug supervisor: Colin Watson
+
+== Setting Upstream Bug Supervisor ==
+
+Setting the bug supervisor for an upstream requires launchpad.Edit
+permission on the product. But regular users can only appoint
+themselves as bug supervisors and teams they administer.
+
+    >>> sample_browser = setupBrowser()
+    >>> sample_browser.addHeader("Authorization",
+    ...                          "Basic test@xxxxxxxxxxxxx:test")
+
+    >>> sample_browser.open(
+    ...     "http://bugs.launchpad.dev/firefox/+bugsupervisor";)
+    >>> sample_browser.getControl(name="field.bug_supervisor").value = (
+    ...                           "test@xxxxxxxxxxxxx")
+    >>> sample_browser.getControl("Change").click()
+
+He is now redirected to the main product page, and he sees a confirmation
+message.
+
+    >>> print extract_text(find_tag_by_id(
+    ...     sample_browser.contents, 'bug-supervisor'))
+    Bug supervisor: Sample Person
+
+Another example, this time with a team that has no "preferred email" set.
+
+    >>> sample_browser.open(
+    ...     "http://bugs.launchpad.dev/firefox/+bugsupervisor";)
+    >>> sample_browser.getControl(name="field.bug_supervisor").value = (
+    ...     "landscape-developers")
+    >>> sample_browser.getControl("Change").click()
+    >>> print extract_text(find_tag_by_id(
+    ...     sample_browser.contents, 'bug-supervisor'))
+    Bug supervisor: Landscape Developers
+
+Launchpad administrators can appoint anybody.
+
+    >>> admin_browser = setupBrowser()
+    >>> admin_browser.addHeader("Authorization",
+    ...                          "Basic foo.bar@xxxxxxxxxxxxx:test")
+
+    >>> admin_browser.open("http://bugs.launchpad.dev/firefox/+bugsupervisor";)
+    >>> admin_browser.getControl(name="field.bug_supervisor").value = (
+    ...                           "robertc@xxxxxxxxxxxxxxxxx")
+    >>> admin_browser.getControl("Change").click()
+    >>> print extract_text(find_tag_by_id(
+    ...     admin_browser.contents, 'bug-supervisor'))
+    Bug supervisor: Robert Collins
+
+Filing a public bug on an upstream will subscribe the bug supervisor, as
+well.
+
+    >>> browser.addHeader("Authorization", "Basic mark@xxxxxxxxxxx:test")
+
+    >>> browser.open("http://launchpad.dev/firefox/+filebug";)
+
+    >>> browser.getControl(name="field.title", index=0).value = "bug supervisor test"
+    >>> browser.getControl('Continue').click()
+
+    >>> browser.getControl(name="field.comment").value = "a public bug"
+    >>> browser.getControl("Submit Bug Report").click()
+
+    >>> bug_id = browser.url.split("/")[-1]
+    >>> print browser.url.replace(bug_id, "BUG-ID")
+    http://bugs.launchpad.dev/firefox/+bug/BUG-ID
+
+Now mark (because he's the bug reporter), Sample Person (a former bug
+supervisor), Landscape Developers (another former bug supervisor) and
+Robert Collins (the current bug supervisor) are subscribed to this bug:
+
+    >>> from itertools import chain
+    >>> from zope.component import getUtility
+    >>> from lp.bugs.interfaces.bug import IBugSet
+    >>> from lp.testing import login, logout, ANONYMOUS
+
+    >>> def subscriber_names(bug):
+    ...     subscribers = chain(
+    ...         bug.getDirectSubscribers(),
+    ...         bug.getIndirectSubscribers())
+    ...     return sorted(subscriber.displayname for subscriber in subscribers)
+
+    >>> login(ANONYMOUS)
+    >>> bugset = getUtility(IBugSet)
+    >>> subscriber_names(bugset.get(bug_id))
+    [u'Landscape Developers', u'Mark Shuttleworth', u'Robert Collins',
+    u'Sample Person']
+
+For a security bug, only the reporter and the registrant gets
+subscribed, because Firefox does not have a security contact.
+
+    >>> from lp.registry.interfaces.product import IProductSet
+
+    >>> login(ANONYMOUS)
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
+    >>> firefox.security_contact is None
+    True
+    >>> logout()
+
+    >>> browser.open("http://launchpad.dev/firefox/+filebug";)
+
+    >>> browser.getControl(name="field.title", index=0).value = "bug supervisor test"
+    >>> browser.getControl('Continue').click()
+
+    >>> browser.getControl(name="field.comment").value = "a PRIVATE bug"
+    >>> browser.getControl("Private Security").selected = True
+    >>> browser.getControl("Submit Bug Report").click()
+
+    >>> other_bug_id = browser.url.split("/")[-1]
+    >>> print browser.url.replace(other_bug_id, "BUG-ID")
+    http://bugs.launchpad.dev/firefox/+bug/BUG-ID
+
+    >>> login("mark@xxxxxxxxxxx")
+
+    >>> subscriber_names(bugset.get(other_bug_id))
+    [u'Mark Shuttleworth', u'Sample Person']
+
+    >>> logout()
+
+Filing a public bug on a distribution source package subscribes the bug
+reporter, the distribution bug supervisor, if there is one, and all the
+package subscribers, if there are any.
+
+    >>> browser.addHeader("Authorization", "Basic mark@xxxxxxxxxxx:test")
+
+    >>> browser.open(
+    ...     "http://localhost:9000/ubuntu/+source/mozilla-firefox/";
+    ...     "+filebug")
+
+    >>> browser.getControl(name="field.title", index=0).value = "a public bug"
+    >>> browser.getControl('Continue').click()
+
+    >>> browser.getControl(name="field.comment").value = (
+    ...     "anyone can see this")
+    >>> browser.getControl("Submit Bug Report").click()
+
+    >>> bug_id = browser.url.split("/")[-1]
+    >>> print browser.url.replace(bug_id, "BUG-ID")
+    http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/BUG-ID
+
+We should have three subscribers now. The bug reporter (also a package
+subscriber), mark, the distro bug supervisor kamion, and foobar, who is
+subscribed to the distribution.
+
+    >>> from zope.component import getUtility
+    >>> from lp.bugs.interfaces.bug import IBugSet
+    >>> from lp.testing import login, logout, ANONYMOUS
+
+    >>> def subscriber_names(bug):
+    ...     subscribers = chain(
+    ...         bug.getDirectSubscribers(),
+    ...         bug.getIndirectSubscribers())
+    ...     return sorted(subscriber.name for subscriber in subscribers)
+
+    >>> login(ANONYMOUS)
+
+    >>> bugset = getUtility(IBugSet)
+    >>> subscriber_names(bugset.get(bug_id))
+    [u'kamion', u'mark', u'name16']
+
+
+When filing a security bug, only the bug reporter and registrant are explicitly
+Cc'd, because the Ubuntu distribution does not have a security contact.
+
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
+    >>> ubuntu.security_contact is None
+    True
+    >>> logout()
+
+    >>> browser.open(
+    ...     "http://localhost:9000/ubuntu/+source/mozilla-firefox/";
+    ...     "+filebug")
+    >>> browser.getControl(name="field.title", index=0).value = "a PRIVATE bug"
+    >>> browser.getControl('Continue').click()
+
+    >>> browser.getControl(name="field.comment").value = "top sekrit"
+    >>> browser.getControl("Private Security").selected = True
+    >>> browser.getControl("Submit Bug Report").click()
+
+    >>> other_bug_id = browser.url.split("/")[-1]
+    >>> print browser.url.replace(other_bug_id, "BUG-ID")
+    http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/BUG-ID
+
+    >>> login("mark@xxxxxxxxxxx")
+
+    >>> subscriber_names(bugset.get(other_bug_id))
+    [u'mark', u'ubuntu-team']
+
+    >>> logout()

=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py	2012-07-17 22:19:31 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py	2012-07-19 04:54:38 +0000
@@ -31,6 +31,7 @@
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
     api_url,
+    celebrity_logged_in,
     launchpadlib_for,
     login_person,
     person_logged_in,
@@ -1045,6 +1046,83 @@
             expected_activity=expected_activity,
             expected_notification=expected_notification)
 
+    def _test_retarget_private_security_bug_to_product(self,
+                                                       bug, maintainer,
+                                                       bug_supervisor=None):
+        # When a private security related bug has a bugtask retargetted to a
+        # different product, a notification is sent to the new bug supervisor
+        # and maintainer. If they are the same person, only one notification
+        # is sent. They only get notifications if they can see the bug.
+
+        # Create the private bug.
+        bug_task = bug.bugtasks[0]
+        new_target = self.factory.makeProduct(
+            owner=maintainer, bug_supervisor=bug_supervisor)
+        self.saveOldChanges(bug)
+
+        bug_task_before_modification = Snapshot(
+            bug_task, providing=providedBy(bug_task))
+        bug_task.transitionToTarget(new_target, self.user)
+        notify(ObjectModifiedEvent(
+            bug_task, bug_task_before_modification,
+            ['target', 'product'], user=self.user))
+
+        expected_activity = {
+            'person': self.user,
+            'whatchanged': 'affects',
+            'oldvalue': bug_task_before_modification.bugtargetname,
+            'newvalue': bug_task.bugtargetname,
+            }
+
+        expected_recipients = [self.user]
+        expected_reasons = ['Subscriber']
+        if bug.userCanView(maintainer):
+            expected_recipients.append(maintainer)
+            expected_reasons.append('Maintainer')
+        if (bug_supervisor and not bug_supervisor.inTeam(maintainer)
+                and bug.userCanView(bug_supervisor)):
+            expected_recipients.append(bug_supervisor)
+            expected_reasons.append('Bug Supervisor')
+        expected_notification = {
+            'text': u"** Project changed: %s => %s" % (
+                bug_task_before_modification.bugtargetname,
+                bug_task.bugtargetname),
+            'person': self.user,
+            'recipients': expected_recipients,
+            'recipient_reasons': expected_reasons
+            }
+        self.assertRecordedChange(
+            expected_activity=expected_activity,
+            expected_notification=expected_notification, bug=bug)
+
+    def test_retarget_private_security_bug_to_product(self):
+        # A series of tests for re-targetting a private bug task.
+        bug = self.factory.makeBug(
+            product=self.product, owner=self.user,
+            information_type=InformationType.USERDATA)
+        maintainer = self.factory.makePerson()
+        bug_supervisor = self.factory.makePerson()
+
+        # Test with no bug supervisor
+        self._test_retarget_private_security_bug_to_product(bug, maintainer)
+        # Test with bug supervisor = maintainer.
+        self._test_retarget_private_security_bug_to_product(
+            bug, maintainer, maintainer)
+        # Test with different bug supervisor
+        self._test_retarget_private_security_bug_to_product(
+            bug, maintainer, bug_supervisor)
+
+        # Now make the bug visible to the bug supervisor and re-test.
+        with celebrity_logged_in('admin'):
+            bug.default_bugtask.transitionToAssignee(bug_supervisor)
+
+        # Test with bug supervisor = maintainer.
+        self._test_retarget_private_security_bug_to_product(
+            bug, maintainer, maintainer)
+        # Test with different bug supervisor
+        self._test_retarget_private_security_bug_to_product(
+            bug, maintainer, bug_supervisor)
+
     def test_target_bugtask_to_sourcepackage(self):
         # When a bugtask's target is changed, BugActivity and
         # BugNotification get updated.
@@ -1096,6 +1174,21 @@
             expected_notification=expected_notification,
             bug=source_package_bug)
 
+    def test_private_bug_target_change_doesnt_add_everyone(self):
+        # Retargeting a private bug doesn't add all subscribers for the
+        # target.
+        old_product = self.factory.makeProduct()
+        new_product = self.factory.makeProduct()
+        subscriber = self.factory.makePerson()
+        new_product.addBugSubscription(subscriber, subscriber)
+        owner = self.factory.makePerson()
+        bug = self.factory.makeBug(
+            product=old_product, owner=owner,
+            information_type=InformationType.USERDATA)
+        bug.default_bugtask.transitionToTarget(new_product, owner)
+        self.assertNotIn(subscriber, bug.getDirectSubscribers())
+        self.assertNotIn(subscriber, bug.getIndirectSubscribers())
+
     def test_add_bugwatch_to_bugtask(self):
         # Adding a BugWatch to a bug task records an entry in
         # BugActivity and BugNotification.

=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
--- lib/lp/bugs/tests/test_bugnotification.py	2012-07-17 22:12:35 +0000
+++ lib/lp/bugs/tests/test_bugnotification.py	2012-07-19 04:54:38 +0000
@@ -7,6 +7,7 @@
 
 from datetime import datetime
 from itertools import chain
+import unittest
 
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
@@ -32,7 +33,6 @@
     )
 from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilterMute
 from lp.registry.enums import InformationType
-from lp.registry.interfaces.accesspolicy import IAccessArtifactGrantSource
 from lp.services.config import config
 from lp.services.messages.interfaces.message import IMessageSet
 from lp.services.messages.model.message import MessageSet
@@ -42,6 +42,7 @@
     TestCaseWithFactory,
     )
 from lp.testing.dbuser import switch_dbuser
+from lp.testing.factory import LaunchpadObjectFactory
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -50,34 +51,33 @@
 from lp.testing.matchers import Contains
 
 
-class TestNotificationRecipientsOfPrivateBugs(TestCaseWithFactory):
+class TestNotificationRecipientsOfPrivateBugs(unittest.TestCase):
     """Test who get notified of changes to private bugs."""
 
     layer = LaunchpadFunctionalLayer
 
     def setUp(self):
-        super(TestNotificationRecipientsOfPrivateBugs, self).setUp()
         login('foo.bar@xxxxxxxxxxxxx')
-        self.product_owner = self.factory.makePerson(name="product-owner")
-        self.product = self.factory.makeProduct(owner=self.product_owner)
-        self.product_subscriber = self.factory.makePerson(
+        factory = LaunchpadObjectFactory()
+        self.product_owner = factory.makePerson(name="product-owner")
+        self.product = factory.makeProduct(owner=self.product_owner)
+        self.product_subscriber = factory.makePerson(
             name="product-subscriber")
         self.product.addBugSubscription(
             self.product_subscriber, self.product_subscriber)
-        self.bug_subscriber = self.factory.makePerson(name="bug-subscriber")
-        self.bug_owner = self.factory.makePerson(name="bug-owner")
-        self.private_bug = self.factory.makeBug(
+        self.bug_subscriber = factory.makePerson(name="bug-subscriber")
+        self.bug_owner = factory.makePerson(name="bug-owner")
+        self.private_bug = factory.makeBug(
             product=self.product, owner=self.bug_owner,
             information_type=InformationType.USERDATA)
         self.reporter = self.private_bug.owner
         self.private_bug.subscribe(self.bug_subscriber, self.reporter)
         [self.product_bugtask] = self.private_bug.bugtasks
-        self.subscribers = set(
+        self.direct_subscribers = set(
             person.name for person in [self.bug_subscriber, self.reporter])
 
     def test_status_change(self):
-        # Status changes are sent to direct and indirect subscribers if they
-        # have access to the bug.
+        # Status changes are sent to the direct subscribers only.
         bugtask_before_modification = Snapshot(
             self.product_bugtask, providing=providedBy(self.product_bugtask))
         self.product_bugtask.transitionToStatus(
@@ -89,41 +89,20 @@
         notified_people = set(
             recipient.person.name
             for recipient in latest_notification.recipients)
-        expected_subscribers = set(
-            person.name for person in [self.bug_subscriber, self.reporter,
-            self.product_subscriber])
-        self.assertEqual(expected_subscribers, notified_people)
+        self.assertEqual(self.direct_subscribers, notified_people)
 
     def test_add_comment(self):
-        # Comment additions are sent to direct and indirect subscribers if
-        # they have access to the bug.
-        self.private_bug.newMessage(
-            self.reporter, subject='subject', content='content')
-        latest_notification = BugNotification.selectFirst(orderBy='-id')
-        notified_people = set(
-            recipient.person.name
-            for recipient in latest_notification.recipients)
-        self.assertEqual(self.subscribers, notified_people)
-
-    def test_add_comment_with_access(self):
-        # If product_subscriber is given access, they are notified.
-        artifact = self.factory.makeAccessArtifact(concrete=self.private_bug)
-        getUtility(IAccessArtifactGrantSource).grant(
-            [(artifact, self.product_subscriber, self.private_bug.owner)])
-        self.private_bug.newMessage(
-            self.reporter, subject='subject', content='content')
-        latest_notification = BugNotification.selectFirst(orderBy='-id')
-        notified_people = set(
-            recipient.person.name
-            for recipient in latest_notification.recipients)
-        expected_subscribers = set(
-            person.name for person in [self.bug_subscriber, self.reporter,
-            self.product_subscriber])
-        self.assertEqual(expected_subscribers, notified_people)
+        # Comment additions are sent to the direct subscribers only.
+        self.private_bug.newMessage(
+            self.reporter, subject='subject', content='content')
+        latest_notification = BugNotification.selectFirst(orderBy='-id')
+        notified_people = set(
+            recipient.person.name
+            for recipient in latest_notification.recipients)
+        self.assertEqual(self.direct_subscribers, notified_people)
 
     def test_bug_edit(self):
-        # Bug edits are sent to direct and indirect subscribers if they have
-        # access to the bug.
+        # Bug edits are sent to direct the subscribers only.
         bug_before_modification = Snapshot(
             self.private_bug, providing=providedBy(self.private_bug))
         self.private_bug.description = 'description'
@@ -134,7 +113,7 @@
         notified_people = set(
             recipient.person.name
             for recipient in latest_notification.recipients)
-        self.assertEqual(self.subscribers, notified_people)
+        self.assertEqual(self.direct_subscribers, notified_people)
 
 
 class TestNotificationsSentForBugExpiration(TestCaseWithFactory):

=== modified file 'lib/lp/registry/tests/test_sharingjob.py'
--- lib/lp/registry/tests/test_sharingjob.py	2012-07-17 22:19:31 +0000
+++ lib/lp/registry/tests/test_sharingjob.py	2012-07-19 04:54:38 +0000
@@ -19,6 +19,7 @@
 from lp.registry.enums import InformationType
 from lp.registry.interfaces.accesspolicy import (
     IAccessArtifactGrantSource,
+    IAccessArtifactSource,
     IAccessPolicySource,
     )
 from lp.registry.interfaces.person import TeamSubscriptionPolicy
@@ -181,9 +182,9 @@
         job, job_type = create_job(distro, bug, grantee, owner)
         # Subscribing grantee has created an artifact grant so we need to
         # revoke that to test the job.
-        artifact = self.factory.makeAccessArtifact(concrete=bug)
         getUtility(IAccessArtifactGrantSource).revokeByArtifact(
-            [artifact], [grantee])
+            getUtility(IAccessArtifactSource).find(
+                [bug]), [grantee])
         transaction.commit()
 
         out, err, exit_code = run_script(
@@ -294,9 +295,9 @@
             CodeReviewNotificationLevel.NOEMAIL, owner)
         # Subscribing policy_team_grantee has created an artifact grant so we
         # need to revoke that to test the job.
-        artifact = self.factory.makeAccessArtifact(concrete=bug)
         getUtility(IAccessArtifactGrantSource).revokeByArtifact(
-            [artifact], [policy_team_grantee])
+            getUtility(IAccessArtifactSource).find(
+                [bug]), [policy_team_grantee])
 
         # policy grantees are subscribed because the job has not been run yet.
         subscribers = removeSecurityProxy(bug).getDirectSubscribers()
@@ -366,9 +367,9 @@
         bug.subscribe(artifact_indirect_grantee, owner)
         # Subscribing policy_team_grantee has created an artifact grant so we
         # need to revoke that to test the job.
-        artifact = self.factory.makeAccessArtifact(concrete=branch)
         getUtility(IAccessArtifactGrantSource).revokeByArtifact(
-            [artifact], [policy_team_grantee])
+            getUtility(IAccessArtifactSource).find(
+                [branch]), [policy_team_grantee])
 
         # policy grantees are subscribed because the job has not been run yet.
         #subscribers = removeSecurityProxy(branch).subscribers
@@ -431,9 +432,10 @@
             bug.subscribe(grantee, owner)
         # Subscribing grantee to bug creates an access grant so we need to
         # revoke that for our test.
-        artifact = self.factory.makeAccessArtifact(concrete=bug)
-        getUtility(IAccessArtifactGrantSource).revokeByArtifact(
-            [artifact], [grantee])
+        accessartifact_source = getUtility(IAccessArtifactSource)
+        accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
+        accessartifact_grant_source.revokeByArtifact(
+            accessartifact_source.find([bug]), [grantee])
 
         return bug, owner
 


Follow ups