← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/private-bug-subscriptions-854405 into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/private-bug-subscriptions-854405 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #854405 in Launchpad itself: "Private bug subscriptions"
  https://bugs.launchpad.net/launchpad/+bug/854405

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/private-bug-subscriptions-854405/+merge/76409

This mp is a re-implementation of the work done for bug 475775 in lp:~wallyworld/launchpad/bug-subscribers-after-private-475775
The original was rolled back due to disagreement about what was implemented. The change over the original functionality in this branch is:

Business rule:
 When a bug is made private any direct subscribers should be unsubscribed unless they are in a small set of authorised users, including pillar owner, bug supervisor etc.
Amendment:
 Only unsubscribe unauthorised direct subscribers if the project is flagged as having private bugs by default

NB I've implemented the above rule as meaning: private_bugs=true for the default_bugtask

== Implementation ==

As well as tweaking the original implementation, another issue was found. The BugSecrecyEditView action handler was where the ObjectModifiedEvent was published when the bug's privacy/security status was changed. There was a listener to this event which subscribed the bug security contact if the bug was changed to security related. However, the view is not the correct place to publish such events - this should be done in the model. And having the view do it means that when the API is used, the expected processing does not occur.

== Tests ==

Unit tests were created to test the subscription behaviour for bugs with and without a private project for the default bug task.
TestBugPrivateAndSecurityRelatedUpdatesPrivateProject
TestBugPrivateAndSecurityRelatedUpdatesPublicProject

Additionally, code in test_bugchanges which used to have to manually publish ObjectModifiedEvent was removed, since these events are now published when setPrivate() and/or setSecurityRelated() are called on the bug model object.

Otherwise, tests are the same as for the previous version of this work.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/browser/bug.py
  lib/lp/bugs/browser/tests/test_bug_views.py
  lib/lp/bugs/browser/tests/test_bugattachment_edit_view.py
  lib/lp/bugs/doc/bug-heat.txt
  lib/lp/bugs/doc/bug.txt
  lib/lp/bugs/doc/bugnotification-email.txt
  lib/lp/bugs/doc/bugnotification-sending.txt
  lib/lp/bugs/doc/bugsubscription.txt
  lib/lp/bugs/doc/initial-bug-contacts.txt
  lib/lp/bugs/doc/security-teams.txt
  lib/lp/bugs/interfaces/bug.py
  lib/lp/bugs/mail/commands.py
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/model/tests/test_bug.py
  lib/lp/bugs/model/tests/test_bugsummary.py
  lib/lp/bugs/scripts/bugimport.py
  lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt
  lib/lp/bugs/subscribers/bug.py
  lib/lp/bugs/tests/test_bugchanges.py
  lib/lp/testing/factory.py

./lib/lp/bugs/doc/bug.txt
     457: source exceeds 78 characters.
    1129: narrative exceeds 78 characters.
    1164: want exceeds 78 characters.
    1165: want exceeds 78 characters.
    1166: want exceeds 78 characters.
./lib/lp/bugs/doc/bugnotification-email.txt
      96: source exceeds 78 characters.
     224: source exceeds 78 characters.
     250: source exceeds 78 characters.
./lib/lp/bugs/doc/bugnotification-sending.txt
     286: want exceeds 78 characters.
./lib/lp/bugs/scripts/bugimport.py
      29: redefinition of unused 'ET' from line 27
./lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt
      27: source has bad indentation.
      30: source exceeds 78 characters.
      38: source has bad indentation.
      46: source has bad indentation.
      51: source has bad indentation.
      66: source exceeds 78 characters.
     122: source exceeds 78 characters.

-- 
https://code.launchpad.net/~wallyworld/launchpad/private-bug-subscriptions-854405/+merge/76409
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/private-bug-subscriptions-854405 into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/browser/bug.py	2011-09-22 02:45:32 +0000
@@ -849,27 +849,19 @@
         data = dict(data)
 
         # We handle privacy changes by hand instead of leaving it to
-        # the usual machinery because we must use bug.setPrivate() to
-        # ensure auditing information is recorded.
+        # the usual machinery because we must use
+        # bug.setPrivacyAndSecurityRelated() to ensure auditing information is
+        # recorded.
         bug = self.context.bug
-        bug_before_modification = Snapshot(
-            bug, providing=providedBy(bug))
         private = data.pop('private')
         user_will_be_subscribed = (
             private and bug.getSubscribersForPerson(self.user).is_empty())
         security_related = data.pop('security_related')
-        private_changed = bug.setPrivate(
-            private, getUtility(ILaunchBag).user)
-        security_related_changed = bug.setSecurityRelated(security_related)
-        if private_changed or security_related_changed:
-            changed_fields = []
-            if private_changed:
-                changed_fields.append('private')
-                self._handlePrivacyChanged(user_will_be_subscribed)
-            if security_related_changed:
-                changed_fields.append('security_related')
-            notify(ObjectModifiedEvent(
-                    bug, bug_before_modification, changed_fields))
+        user = getUtility(ILaunchBag).user
+        (private_changed, security_related_changed) = (
+            bug.setPrivacyAndSecurityRelated(private, security_related, user))
+        if private_changed:
+            self._handlePrivacyChanged(user_will_be_subscribed)
         if self.request.is_ajax:
             if private_changed or security_related_changed:
                 return self._getSubscriptionDetails()

=== modified file 'lib/lp/bugs/browser/tests/test_bug_views.py'
--- lib/lp/bugs/browser/tests/test_bug_views.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/browser/tests/test_bug_views.py	2011-09-22 02:45:32 +0000
@@ -217,7 +217,7 @@
     layer = DatabaseFunctionalLayer
 
     def createInitializedSecrecyView(self, person=None, bug=None,
-                                     request=None):
+                                     request=None, security_related=False):
         """Create and return an initialized BugSecrecyView."""
         if person is None:
             person = self.factory.makePerson()
@@ -227,7 +227,8 @@
             view = create_initialized_view(
                 bug.default_bugtask, name='+secrecy', form={
                     'field.private': 'on',
-                    'field.security_related': '',
+                    'field.security_related':
+                        'on' if security_related else 'off',
                     'field.actions.change': 'Change',
                     },
                 request=request)
@@ -276,7 +277,7 @@
         # privacy as well as information used to populate the updated
         # subscribers list.
         person = self.factory.makePerson()
-        bug = self.factory.makeBug()
+        bug = self.factory.makeBug(owner=person)
         with person_logged_in(person):
             bug.subscribe(person, person)
 
@@ -285,7 +286,7 @@
             method='POST', form={
                 'field.actions.change': 'Change',
                 'field.private': 'on',
-                'field.security_related': 'ff'},
+                'field.security_related': 'off'},
             **extra)
         view = self.createInitializedSecrecyView(person, bug, request)
         result_data = simplejson.loads(view.render())
@@ -301,3 +302,18 @@
             subscription_data['person_link'])
         self.assertEqual(
             'Discussion', subscription_data['bug_notification_level'])
+
+        [subscriber_data] = result_data['subscription_data']
+        subscriber = removeSecurityProxy(bug.default_bugtask).pillar.owner
+        self.assertEqual(
+            subscriber.name, subscriber_data['subscriber']['name'])
+        self.assertEqual('Discussion', subscriber_data['subscription_level'])
+
+    def test_set_security_related(self):
+        # Test that the bug attribute 'security_related' can be updated
+        # using the view.
+        owner = self.factory.makePerson()
+        bug = self.factory.makeBug(owner=owner)
+        self.createInitializedSecrecyView(bug=bug, security_related=True)
+        with person_logged_in(owner):
+            self.assertTrue(bug.security_related)

=== modified file 'lib/lp/bugs/browser/tests/test_bugattachment_edit_view.py'
--- lib/lp/bugs/browser/tests/test_bugattachment_edit_view.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/browser/tests/test_bugattachment_edit_view.py	2011-09-22 02:45:32 +0000
@@ -9,6 +9,7 @@
 from canonical.testing import LaunchpadFunctionalLayer
 from lp.testing import (
     login_person,
+    person_logged_in,
     TestCaseWithFactory,
     )
 from lp.testing.views import create_initialized_view
@@ -54,8 +55,9 @@
     def test_change_action_private_bug(self):
         # Subscribers of a private bug can edit attachments.
         user = self.factory.makePerson()
-        self.bug.subscribe(user, self.bug_owner)
         self.bug.setPrivate(True, self.bug_owner)
+        with person_logged_in(self.bug_owner):
+            self.bug.subscribe(user, self.bug_owner)
         transaction.commit()
         login_person(user)
         create_initialized_view(
@@ -90,8 +92,9 @@
     def test_delete_action_private_bug(self):
         # Subscribers of a private bug can delete attachments.
         user = self.factory.makePerson()
-        self.bug.subscribe(user, self.bug_owner)
         self.bug.setPrivate(True, self.bug_owner)
+        with person_logged_in(self.bug_owner):
+            self.bug.subscribe(user, self.bug_owner)
         login_person(user)
         create_initialized_view(
             self.bugattachment, name='+edit', form=self.DELETE_FORM_DATA)

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/configure.zcml	2011-09-22 02:45:32 +0000
@@ -844,6 +844,7 @@
                     removeWatch
                     setPrivate
                     setSecurityRelated
+                    setPrivacyAndSecurityRelated
                     setStatus
                     subscribe
                     unlinkBranch

=== modified file 'lib/lp/bugs/doc/bug-heat.txt'
--- lib/lp/bugs/doc/bug-heat.txt	2011-09-19 13:43:07 +0000
+++ lib/lp/bugs/doc/bug-heat.txt	2011-09-22 02:45:32 +0000
@@ -80,52 +80,52 @@
 
     >>> changed = bug.setPrivate(True, bug_owner)
     >>> bug.heat
-    156
+    158
 
 Setting the bug as security related adds another 250 heat points.
 
-    >>> changed = bug.setSecurityRelated(True)
+    >>> changed = bug.setSecurityRelated(True, bug_owner)
     >>> bug.heat
-    406
+    408
 
 Marking the bug public removes 150 heat points.
 
     >>> changed = bug.setPrivate(False, bug_owner)
     >>> bug.heat
-    256
+    258
 
 And marking it not security-related removes 250 points.
 
-    >>> changed = bug.setSecurityRelated(False)
+    >>> changed = bug.setSecurityRelated(False, bug_owner)
     >>> bug.heat
-    6
+    8
 
 Adding a subscriber to the bug increases its heat by 2 points.
 
     >>> new_subscriber = factory.makePerson()
     >>> subscription = bug.subscribe(new_subscriber, new_subscriber)
     >>> bug.heat
-    8
+    10
 
 When a user unsubscribes, the bug loses 2 points of heat.
 
     >>> bug.unsubscribe(new_subscriber, new_subscriber)
     >>> bug.heat
-    6
+    8
 
 Should a user mark themselves as affected by the bug, it will gain 4
 points of heat.
 
     >>> bug.markUserAffected(new_subscriber)
     >>> bug.heat
-    10
+    12
 
 If a user who was previously affected marks themself as not affected,
 the bug loses 4 points of heat.
 
     >>> bug.markUserAffected(new_subscriber, False)
     >>> bug.heat
-    6
+    8
 
 If a user who wasn't affected by the bug marks themselve as explicitly
 unaffected, the bug's heat doesn't change.
@@ -133,7 +133,7 @@
     >>> unaffected_person = factory.makePerson()
     >>> bug.markUserAffected(unaffected_person, False)
     >>> bug.heat
-    6
+    8
 
 Marking the bug as a duplicate will set its heat to zero, whilst also
 adding 10 points of heat to the bug it duplicates, 6 points for the
@@ -149,14 +149,14 @@
     0
 
     >>> duplicated_bug.heat
-    14
+    16
 
 Unmarking the bug as a duplicate restores its heat and updates the
 duplicated bug's heat.
 
     >>> bug.markAsDuplicate(None)
     >>> bug.heat
-    6
+    8
 
     >>> duplicated_bug.heat
     6
@@ -185,7 +185,7 @@
     ...     old_value=bug.description, new_value='Some text')
     >>> bug.addChange(change)
     >>> bug.heat
-    6
+    8
 
 
 Getting bugs whose heat is outdated

=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/doc/bug.txt	2011-09-22 02:45:32 +0000
@@ -750,7 +750,8 @@
 
     >>> firefox_bug.security_related
     False
-    >>> changed = firefox_bug.setSecurityRelated(True)
+    >>> changed = firefox_bug.setSecurityRelated(True,
+    ...     getUtility(ILaunchBag).user)
 
     >>> bug_security_changed = ObjectModifiedEvent(
     ...     firefox_bug, bug_before_modification, ["security_related"])

=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
--- lib/lp/bugs/doc/bugnotification-email.txt	2011-09-19 13:43:07 +0000
+++ lib/lp/bugs/doc/bugnotification-email.txt	2011-09-22 02:45:32 +0000
@@ -93,7 +93,7 @@
 
 New security related bugs are sent with a prominent warning:
 
-    >>> changed = bug_four.setSecurityRelated(True)
+    >>> changed = bug_four.setSecurityRelated(True, getUtility(ILaunchBag).user)
 
     >>> subject, body = generate_bug_add_email(bug_four)
     >>> subject
@@ -210,7 +210,7 @@
     + edit notification email generator knows how to indent and wrap
     + descriptions, so this will appear quite nice in the actual email that
     + gets sent.
-    + 
+    +
     + It's also smart enough to preserve whitespace, finally!
     -----------------------------
 
@@ -221,8 +221,7 @@
 
     >>> edited_bug.setPrivate(True, getUtility(ILaunchBag).user)
     True
-
-    >>> changed = edited_bug.setSecurityRelated(True)
+    >>> changed = edited_bug.setSecurityRelated(True, getUtility(ILaunchBag).user)
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
     ...     bugurl="http://www.example.com/bugs/2";,
@@ -248,8 +247,7 @@
 
     >>> edited_bug.setPrivate(False, getUtility(ILaunchBag).user)
     True
-
-    >>> changed = edited_bug.setSecurityRelated(False)
+    >>> changed = edited_bug.setSecurityRelated(False, getUtility(ILaunchBag).user)
     >>> bug_delta = BugDelta(
     ...     bug=edited_bug,
     ...     bugurl="http://www.example.com/bugs/2";,

=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
--- lib/lp/bugs/doc/bugnotification-sending.txt	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/doc/bugnotification-sending.txt	2011-09-22 02:45:32 +0000
@@ -768,8 +768,13 @@
     >>> bug_three.private
     True
 
+When the bug is made private, the bugtask pillar owners and bug supervisors
+are subscribed in addition any other direct subscribers which already exist.
+
     >>> for message in trigger_and_get_email_messages(bug_three):
     ...     print message['To'], message.get_all('X-Launchpad-Bug-Private')
+    foo.bar@xxxxxxxxxxxxx ['yes']
+    mark@xxxxxxxxxxx ['yes']
     test@xxxxxxxxxxxxx ['yes']
 
 
@@ -786,13 +791,15 @@
     >>> for message in trigger_and_get_email_messages(bug_three):
     ...     print message['To'], (
     ...         message.get_all('X-Launchpad-Bug-Security-Vulnerability'))
+    foo.bar@xxxxxxxxxxxxx ['no']
+    mark@xxxxxxxxxxx ['no']
     test@xxxxxxxxxxxxx ['no']
 
 The presence of the security flag on a bug is, surprise, denoted by a
 simple "yes":
 
     >>> with lp_dbuser():
-    ...     bug_three.setSecurityRelated(True)
+    ...     bug_three.setSecurityRelated(True, getUtility(ILaunchBag).user)
     True
     >>> bug_three.security_related
     True
@@ -800,6 +807,8 @@
     >>> for message in trigger_and_get_email_messages(bug_three):
     ...     print message['To'], (
     ...         message.get_all('X-Launchpad-Bug-Security-Vulnerability'))
+    foo.bar@xxxxxxxxxxxxx ['yes']
+    mark@xxxxxxxxxxx ['yes']
     test@xxxxxxxxxxxxx ['yes']
 
 
@@ -810,8 +819,8 @@
 people who have ever commented on the bug. It's a space-separated
 list.
 
-    >>> for message in trigger_and_get_email_messages(bug_three):
-    ...     print message.get('X-Launchpad-Bug-Commenters')
+    >>> message = trigger_and_get_email_messages(bug_three)[0]
+    >>> print message.get('X-Launchpad-Bug-Commenters')
     name12
 
     >>> from lp.registry.interfaces.person import IPersonSet
@@ -822,8 +831,8 @@
     ...     ignored = getUtility(IBugMessageSet).createMessage(
     ...         'Hungry', bug_three, foo_bar, "Make me a sandwich.")
 
-    >>> for message in trigger_and_get_email_messages(bug_three):
-    ...     print message.get('X-Launchpad-Bug-Commenters')
+    >>> message = trigger_and_get_email_messages(bug_three)[0]
+    >>> print message.get('X-Launchpad-Bug-Commenters')
     name12 name16
 
 It only lists each user once, no matter how many comments they've
@@ -833,8 +842,8 @@
     ...     ignored = getUtility(IBugMessageSet).createMessage(
     ...         'Hungry', bug_three, foo_bar, "Make me a sandwich.")
 
-    >>> for message in trigger_and_get_email_messages(bug_three):
-    ...     print message.get('X-Launchpad-Bug-Commenters')
+    >>> message = trigger_and_get_email_messages(bug_three)[0]
+    >>> print message.get('X-Launchpad-Bug-Commenters')
     name12 name16
 
 
@@ -844,8 +853,8 @@
 The X-Launchpad-Bug-Reporter header contains information about the Launchpad
 user who originally reported the bug and opened the bug's first bug task.
 
-    >>> for message in trigger_and_get_email_messages(bug_three):
-    ...     print message.get('X-Launchpad-Bug-Reporter')
+    >>> message = trigger_and_get_email_messages(bug_three)[0]
+    >>> print message.get('X-Launchpad-Bug-Reporter')
     Foo Bar (name16)
 
 
@@ -1176,7 +1185,7 @@
     ----------------------------------------------------------------------
     To: owner@xxxxxxxxxxx
     ...
-    You received this bug notification because you are a member of 
+    You received this bug notification because you are a member of
     Addressless Team, which is subscribed to the bug report.
     ...
     ----------------------------------------------------------------------
@@ -1240,7 +1249,7 @@
     ----------------------------------------------------------------------
     To: owner@xxxxxxxxxxx
     ...
-    You received this bug notification because you are a member of 
+    You received this bug notification because you are a member of
     Addressless Team, which is subscribed to the bug report.
     ...
     ----------------------------------------------------------------------
@@ -1293,7 +1302,7 @@
     no comment for no-priv.
     <BLANKLINE>
     --
-    You received this bug notification because you are a member of 
+    You received this bug notification because you are a member of
     Addressless Team, which is subscribed to the bug report.
     ...
     ----------------------------------------------------------------------
@@ -1357,7 +1366,7 @@
     ----------------------------------------------------------------------
     To: owner@xxxxxxxxxxx
     ...
-    You received this bug notification because you are a member of 
+    You received this bug notification because you are a member of
     Addressless Team, which is subscribed to the bug report.
     ...
     ----------------------------------------------------------------------
@@ -1412,7 +1421,7 @@
     + Whatever else
     <BLANKLINE>
     --
-    You received this bug notification because you are a member of 
+    You received this bug notification because you are a member of
     Addressless Team, which is subscribed to the bug report.
     http://bugs.launchpad.dev/bugs/1
     ...
@@ -1479,7 +1488,7 @@
     ----------------------------------------------------------------------
     To: owner@xxxxxxxxxxx
     ...
-    You received this bug notification because you are a member of 
+    You received this bug notification because you are a member of
     Addressless Team, which is subscribed to the bug report.
     ...
     ----------------------------------------------------------------------

=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt	2011-09-22 02:45:32 +0000
@@ -379,8 +379,9 @@
     >>> linux_source_bug.getSubscribersFromDuplicates()
     ()
 
-When a bug is marked private, all its indirect subscribers become direct
-subscribers.
+When a bug is marked private, specific people like the bugtask bug supervisors
+will be automatically subscribed, and only specifically allowed existing
+direct subscribers (eg bugtask pillar maintainers) will remain subscribed.
 
     >>> from zope.event import notify
 
@@ -403,16 +404,11 @@
     >>> print_displayname(linux_source_bug.getDirectSubscribers())
     Foo Bar
     Mark Shuttleworth
-    No Privileges Person
     Robert Collins
-    Sample Person
-    Scott James Remnant
     Ubuntu Team
 
-A private bug never has indirect subscribers. Since all our indirect
-subscribers have been made into direct subscribers, let's add another
-indirect subscriber to show that they still aren't included in the
-indirect subscriptions.
+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"))
@@ -423,9 +419,9 @@
     >>> 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 only martin-pitt.
+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.
 
     >>> linux_source_bug.setPrivate(False, getUtility(ILaunchBag).user)
     True
@@ -434,17 +430,20 @@
     >>> print_displayname(linux_source_bug.getDirectSubscribers())
     Foo Bar
     Mark Shuttleworth
-    No Privileges Person
     Robert Collins
-    Sample Person
-    Scott James Remnant
     Ubuntu Team
 
     >>> print_displayname(linux_source_bug.getIndirectSubscribers())
     Martin Pitt
+    No Privileges Person
+    Sample Person
+    Scott James Remnant
 
     >>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
     Martin Pitt
+    No Privileges Person
+    Sample Person
+    Scott James Remnant
 
 To find out which email addresses should receive a notification email on
 a bug, and why, IBug.getBugNotificationRecipients() assembles an
@@ -458,18 +457,14 @@
     >>> [(address, recipients.getReason(address)[1]) for address in addresses]
     [('foo.bar@xxxxxxxxxxxxx', 'Subscriber'),
      ('mark@xxxxxxxxxxx', 'Subscriber'),
-     ('no-priv@xxxxxxxxxxxxx', 'Subscriber'),
+     ('no-priv@xxxxxxxxxxxxx', u'Subscriber (linux-source-2.6.15 in Ubuntu)'),
      ('robertc@xxxxxxxxxxxxxxxxx', 'Subscriber'),
      ('support@xxxxxxxxxx', u'Subscriber @ubuntu-team'),
-     ('test@xxxxxxxxxxxxx', 'Subscriber')]
+     ('test@xxxxxxxxxxxxx', 'Assignee')]
 
 If IBug.getBugNotificationRecipients() is passed a  BugNotificationLevel
 in its `level` parameter, only structural subscribers with that
-notification level or higher will be returned. When the linux_source_bug
-was temporarily set to "private", the structural subscriber Sample Person
-was directly subscribed, thus he is returned by
-getBugNotificationRecipients() even if the parameter level is larger than
-his structural subscription setting.
+notification level or higher will be returned.
 
     >>> recipients = linux_source_bug.getBugNotificationRecipients(
     ...     level=BugNotificationLevel.COMMENTS)
@@ -477,10 +472,9 @@
     >>> [(address, recipients.getReason(address)[1]) for address in addresses]
     [('foo.bar@xxxxxxxxxxxxx', 'Subscriber'),
      ('mark@xxxxxxxxxxx', 'Subscriber'),
-     ('no-priv@xxxxxxxxxxxxx', 'Subscriber'),
      ('robertc@xxxxxxxxxxxxxxxxx', 'Subscriber'),
      ('support@xxxxxxxxxx', u'Subscriber @ubuntu-team'),
-     ('test@xxxxxxxxxxxxx', 'Subscriber')]
+     ('test@xxxxxxxxxxxxx', 'Assignee')]
 
 When Sample Person is unsubscribed from linux_source_bug, he is no
 longer included in the result of getBugNotificationRecipients() for
@@ -495,7 +489,7 @@
      ('mark@xxxxxxxxxxx', 'Subscriber'),
      ('robertc@xxxxxxxxxxxxxxxxx', 'Subscriber'),
      ('support@xxxxxxxxxx', u'Subscriber @ubuntu-team'),
-     ('test@xxxxxxxxxxxxx', 'Subscriber')]
+     ('test@xxxxxxxxxxxxx', 'Assignee')]
 
 ...but remains included for the level LIFECYCLE.
 
@@ -509,14 +503,15 @@
      ('no-priv@xxxxxxxxxxxxx', u'Subscriber (linux-source-2.6.15 in Ubuntu)'),
      ('robertc@xxxxxxxxxxxxxxxxx', 'Subscriber'),
      ('support@xxxxxxxxxx', u'Subscriber @ubuntu-team'),
-     ('test@xxxxxxxxxxxxx', 'Subscriber')]
+     ('test@xxxxxxxxxxxxx', 'Assignee')]
 
 To find out if someone is already directly subscribed to a bug, call
 IBug.isSubscribed, passing in an IPerson:
 
     >>> linux_source_bug.isSubscribed(personset.getByName("debonzi"))
     False
-    >>> linux_source_bug.isSubscribed(sample_person)
+    >>> name16 = personset.getByName("name16")
+    >>> linux_source_bug.isSubscribed(name16)
     True
 
 Call isSubscribedToDupes to see if a user is directly subscribed to

=== modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt'
--- lib/lp/bugs/doc/initial-bug-contacts.txt	2011-09-19 13:43:07 +0000
+++ lib/lp/bugs/doc/initial-bug-contacts.txt	2011-09-22 02:45:32 +0000
@@ -316,12 +316,11 @@
     >>> transaction.commit()
     >>> stub.test_emails = []
 
-the subscriptions remain unchanged:
+the unallowed subscribers are removed:
 
     >>> subscriber_names(bug_one_in_ubuntu_firefox.bug)
-    [u'Dafydd Harries', u'Foo Bar', u'Mark Shuttleworth',
-     u'Sample Person', u'Steve Alexander', u'Ubuntu Gnome Team',
-     u'Ubuntu Team']
+    [u'Foo Bar', u'Mark Shuttleworth', u'Sample Person',
+     u'Steve Alexander', u'Ubuntu Team']
 
 
 Product Bug Supervisors and Bug Tasks

=== modified file 'lib/lp/bugs/doc/security-teams.txt'
--- lib/lp/bugs/doc/security-teams.txt	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/doc/security-teams.txt	2011-09-22 02:45:32 +0000
@@ -265,7 +265,7 @@
     >>> subscriber_names(bug)
     [u'name12', u'name16']
 
-    >>> bug.setSecurityRelated(False)
+    >>> bug.setSecurityRelated(False, getUtility(ILaunchBag).user)
     True
 
     >>> bug.security_related
@@ -296,7 +296,7 @@
     >>> bug.addTask(owner=reporter, target=distribution)
     <BugTask ...>
     >>> old_state = Snapshot(bug, providing=IBug)
-    >>> bug.setSecurityRelated(True)
+    >>> bug.setSecurityRelated(True, getUtility(ILaunchBag).user)
     True
     >>> notify(ObjectModifiedEvent(bug, old_state, ['security_related']))
     >>> for subscriber_name in sorted(

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/interfaces/bug.py	2011-09-22 02:45:32 +0000
@@ -850,11 +850,13 @@
 
     @mutator_for(security_related)
     @operation_parameters(security_related=copy_field(security_related))
+    @call_with(who=REQUEST_USER)
     @export_write_operation()
-    def setSecurityRelated(security_related):
+    def setSecurityRelated(security_related, who):
         """Set bug security.
 
             :security_related: True/False.
+            :who: The IPerson who is making the change.
 
         This may also cause the security contact to be subscribed
         if one is registered and if the bug is not private.
@@ -862,6 +864,23 @@
         Return True if a change is made, False otherwise.
         """
 
+    @operation_parameters(
+        private=copy_field(private),
+        security_related=copy_field(security_related),
+        )
+    @call_with(who=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version("devel")
+    def setPrivacyAndSecurityRelated(private, security_related, who):
+        """Set bug privacy and security .
+
+            :private: True/False.
+            :security_related: True/False.
+            :who: The IPerson who is making the change.
+
+        Return (private_changed, security_related_changed) tuple.
+        """
+
     def getBugTask(target):
         """Return the bugtask with the specified target.
 

=== modified file 'lib/lp/bugs/mail/commands.py'
--- lib/lp/bugs/mail/commands.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/mail/commands.py	2011-09-22 02:45:32 +0000
@@ -256,13 +256,13 @@
                 context, providing=providedBy(context))
 
         # Apply requested changes.
+        user = getUtility(ILaunchBag).user
         if security_related:
-            user = getUtility(ILaunchBag).user
             if context.setPrivate(True, user):
                 edited = True
                 edited_fields.add('private')
         if context.security_related != security_related:
-            context.setSecurityRelated(security_related)
+            context.setSecurityRelated(security_related, user)
             edited = True
             edited_fields.add('security_related')
 

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/model/bug.py	2011-09-22 02:45:32 +0000
@@ -811,7 +811,7 @@
         self.updateHeat()
         return sub
 
-    def unsubscribe(self, person, unsubscribed_by):
+    def unsubscribe(self, person, unsubscribed_by, **kwargs):
         """See `IBug`."""
         # Drop cached subscription info.
         clear_property_cache(self)
@@ -821,9 +821,11 @@
         if person is None:
             person = unsubscribed_by
 
+        ignore_permissions = kwargs.get('ignore_permissions', False)
         for sub in self.subscriptions:
             if sub.person.id == person.id:
-                if not sub.canBeUnsubscribedByUser(unsubscribed_by):
+                if (not ignore_permissions
+                        and not sub.canBeUnsubscribedByUser(unsubscribed_by)):
                     raise UserCannotUnsubscribePerson(
                         '%s does not have permission to unsubscribe %s.' % (
                             unsubscribed_by.displayname,
@@ -1644,28 +1646,20 @@
 
         return bugtask
 
-    def setPrivate(self, private, who):
-        """See `IBug`.
-
-        We also record who made the change and when the change took
-        place.
-        """
+    def setPrivacyAndSecurityRelated(self, private, security_related, who):
+        """ See `IBug`."""
+        private_changed = False
+        security_related_changed = False
+        bug_before_modification = Snapshot(self, providing=providedBy(self))
+
+        # Before we update the privacy or security_related status, we need to
+        # reconcile the subscribers to avoid leaking private information.
+        if (self.private != private
+                or self.security_related != security_related):
+            self.reconcileSubscribers(private, security_related, who)
+
         if self.private != private:
-            if private:
-                # Change indirect subscribers into direct subscribers
-                # *before* setting private because
-                # getIndirectSubscribers() behaves differently when
-                # the bug is private.
-                for person in self.getIndirectSubscribers():
-                    self.subscribe(person, who)
-                subscribers_for_who = self.getSubscribersForPerson(who)
-                if subscribers_for_who.is_empty():
-                    # We also add `who` as a subscriber, if they're not
-                    # already directly subscribed or part of a team
-                    # that's directly subscribed, so that they can
-                    # see the bug they've just marked private.
-                    self.subscribe(who, who)
-
+            private_changed = True
             self.private = private
 
             if private:
@@ -1680,25 +1674,116 @@
             for attachment in self.attachments_unpopulated:
                 attachment.libraryfile.restricted = private
 
-            # Correct the heat for the bug immediately, so that we don't have
-            # to wait for the next calculation job for the adjusted heat.
-            self.updateHeat()
-            return True  # Changed.
-        else:
-            return False  # Not changed.
-
-    def setSecurityRelated(self, security_related):
-        """Setter for the `security_related` property."""
         if self.security_related != security_related:
+            security_related_changed = True
             self.security_related = security_related
 
+        if private_changed or security_related_changed:
             # Correct the heat for the bug immediately, so that we don't have
             # to wait for the next calculation job for the adjusted heat.
             self.updateHeat()
 
-            return True  # Changed
-        else:
-            return False  # Unchanged
+        if private_changed or security_related_changed:
+            changed_fields = []
+            if private_changed:
+                changed_fields.append('private')
+            if security_related_changed:
+                changed_fields.append('security_related')
+            notify(ObjectModifiedEvent(
+                    self, bug_before_modification, changed_fields, user=who))
+
+        return private_changed, security_related_changed
+
+    def setPrivate(self, private, who):
+        """See `IBug`.
+
+        We also record who made the change and when the change took
+        place.
+        """
+        return self.setPrivacyAndSecurityRelated(
+            private, self.security_related, who)[0]
+
+    def setSecurityRelated(self, security_related, who):
+        """Setter for the `security_related` property."""
+        return self.setPrivacyAndSecurityRelated(
+            self.private, security_related, who)[1]
+
+    def getRequiredSubscribers(self, for_private, for_security_related, who):
+        """Return the mandatory subscribers for a bug with given attributes.
+
+        When a bug is marked as private or security related, it is required
+        that certain people be subscribed so they can access details about the
+        bug. The rules are:
+            security=true, private=true/false ->
+                subscribers = the reporter + security contact for each task
+            security=false, private=true ->
+                subscribers = the reporter + bug supervisor for each task
+            security=false, private=false ->
+                subscribers = ()
+
+        If bug supervisor or security contact is unset, fallback to bugtask
+        reporter/owner.
+        """
+        if not for_private and not for_security_related:
+            return set()
+        result = set()
+        result.add(self.owner)
+        for bugtask in self.bugtasks:
+            maintainer = bugtask.pillar.owner
+            if for_security_related:
+                result.add(bugtask.pillar.security_contact or maintainer)
+            if for_private:
+                result.add(bugtask.pillar.bug_supervisor or maintainer)
+        if for_private:
+            subscribers_for_who = self.getSubscribersForPerson(who)
+            if subscribers_for_who.is_empty():
+                result.add(who)
+        return result
+
+    def reconcileSubscribers(self, for_private, for_security_related, who):
+        """ Ensure only appropriate people are subscribed to private bugs.
+
+        When a bug is marked as either private = True or security_related =
+        True, we need to ensure that only people who are authorised to know
+        about the privileged contents of the bug remain directly subscribed
+        to it. So we:
+          1. Get the required subscribers depending on the bug status.
+          2. Get the auto removed subscribers depending on the bug status.
+             eg security contacts when a bug is updated to security related =
+             false.
+          3. Get the allowed subscribers = required subscribers
+                                            + bugtask owners
+          4. Remove any current direct subscribers who are not allowed or are
+             to be auto removed.
+          5. Add any subscribers who are required.
+        """
+        current_direct_subscribers = (
+            self.getSubscriptionInfo().direct_subscribers)
+        required_subscribers = self.getRequiredSubscribers(
+            for_private, for_security_related, who)
+        allowed_subscribers = set()
+        allowed_subscribers.add(self.owner)
+        for bugtask in self.bugtasks:
+            allowed_subscribers.add(bugtask.owner)
+            allowed_subscribers.add(bugtask.pillar.owner)
+            allowed_subscribers.update(set(bugtask.pillar.drivers))
+        allowed_subscribers = required_subscribers.union(allowed_subscribers)
+
+        # If this bug is for a project that is marked as having private bugs
+        # by default, and the bug is private or security related, we will
+        # unsubscribe any unauthorised direct subscribers.
+        pillar = self.default_bugtask.pillar
+        private_project = IProduct.providedBy(pillar) and pillar.private_bugs
+        if private_project and (for_private or for_security_related):
+            subscribers_to_remove = (
+                current_direct_subscribers.difference(allowed_subscribers))
+            for subscriber in subscribers_to_remove:
+                self.unsubscribe(subscriber, who, ignore_permissions=True)
+
+        subscribers_to_add = (
+            required_subscribers.difference(current_direct_subscribers))
+        for subscriber in subscribers_to_add:
+            self.subscribe(subscriber, who)
 
     def getBugTask(self, target):
         """See `IBug`."""

=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/model/tests/test_bug.py	2011-09-22 02:45:32 +0000
@@ -1,11 +1,14 @@
 # Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
+from canonical.launchpad.interfaces.lpstorm import IStore
+from lp.bugs.model.bugnotification import BugNotificationRecipient
 
 __metaclass__ = type
 
 from storm.store import Store
 from testtools.testcase import ExpectedException
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.bugs.enum import (
@@ -434,27 +437,6 @@
             info = bug.getSubscriptionInfo(BugNotificationLevel.METADATA)
         self.assertEqual(BugNotificationLevel.METADATA, info.level)
 
-    def test_setPrivate_subscribes_person_who_makes_bug_private(self):
-        # When setPrivate(True) is called on a bug, the person who is
-        # marking the bug private is subscribed to the bug.
-        bug = self.factory.makeBug()
-        person = self.factory.makePerson()
-        with person_logged_in(person):
-            bug.setPrivate(True, person)
-            self.assertTrue(bug.personIsDirectSubscriber(person))
-
-    def test_setPrivate_does_not_subscribe_member_of_subscribed_team(self):
-        # When setPrivate(True) is called on a bug, the person who is
-        # marking the bug private will not be subscribed if they're
-        # already a member of a team which is a direct subscriber.
-        bug = self.factory.makeBug()
-        team = self.factory.makeTeam()
-        person = team.teamowner
-        with person_logged_in(person):
-            bug.subscribe(team, person)
-            bug.setPrivate(True, person)
-            self.assertFalse(bug.personIsDirectSubscriber(person))
-
     def test_getVisibleLinkedBranches_doesnt_rtn_inaccessible_branches(self):
         # If a Bug has branches linked to it that the current user
         # cannot access, those branches will not be returned in its
@@ -485,6 +467,253 @@
         self.assertNotIn(private_branch, linked_branches)
 
 
+class TestBugPrivateAndSecurityRelatedUpdatesMixin:
+
+    layer = DatabaseFunctionalLayer
+
+    def test_setPrivate_subscribes_person_who_makes_bug_private(self):
+        # When setPrivate(True) is called on a bug, the person who is
+        # marking the bug private is subscribed to the bug.
+        bug = self.factory.makeBug()
+        person = self.factory.makePerson()
+        with person_logged_in(person):
+            bug.setPrivate(True, person)
+            self.assertTrue(bug.personIsDirectSubscriber(person))
+
+    def test_setPrivate_does_not_subscribe_member_of_subscribed_team(self):
+        # When setPrivate(True) is called on a bug, the person who is
+        # marking the bug private will not be subscribed if they're
+        # already a member of a team which is a direct subscriber.
+        bug = self.factory.makeBug()
+        team = self.factory.makeTeam()
+        person = team.teamowner
+        with person_logged_in(person):
+            bug.subscribe(team, person)
+            bug.setPrivate(True, person)
+            self.assertFalse(bug.personIsDirectSubscriber(person))
+
+    def createBugTasksAndSubscribers(self, private_security_related=False):
+        # Used with the various setPrivateAndSecurityRelated tests to create
+        # a bug and add some initial subscribers.
+        bug_owner = self.factory.makePerson(name='bugowner')
+        bug_supervisor = self.factory.makePerson(name='bugsupervisor')
+        product_owner = self.factory.makePerson(name='productowner')
+        bug_product = self.factory.makeProduct(
+            owner=product_owner, bug_supervisor=bug_supervisor)
+        if self.private_project:
+            removeSecurityProxy(bug_product).private_bugs = True
+        bug = self.factory.makeBug(
+            owner=bug_owner,
+            product=bug_product,
+            private=private_security_related,
+            security_related=private_security_related)
+        owner_a = self.factory.makePerson(name='ownera')
+        security_contact_a = self.factory.makePerson(name='securitycontacta')
+        bug_supervisor_a = self.factory.makePerson(name='bugsupervisora')
+        driver_a = self.factory.makePerson(name='drivera')
+        product_a = self.factory.makeProduct(
+            owner=owner_a,
+            security_contact=security_contact_a,
+            bug_supervisor=bug_supervisor_a,
+            driver=driver_a)
+        owner_b = self.factory.makePerson(name='ownerb')
+        security_contact_b = self.factory.makePerson(name='securitycontactb')
+        product_b = self.factory.makeProduct(
+            owner=owner_b,
+            security_contact=security_contact_b)
+        bugtask_a = self.factory.makeBugTask(bug=bug, target=product_a)
+        bugtask_b = self.factory.makeBugTask(bug=bug, target=product_b)
+        naked_bugtask_a = removeSecurityProxy(bugtask_a)
+        naked_bugtask_b = removeSecurityProxy(bugtask_b)
+        naked_default_bugtask = removeSecurityProxy(bug.default_bugtask)
+        return (bug, bug_owner, naked_bugtask_a, naked_bugtask_b,
+                naked_default_bugtask)
+
+    def test_setPrivateTrueAndSecurityRelatedTrue(self):
+        # When a bug is marked as private=true and security_related=true, the
+        # direct subscribers should include:
+        # - the bug reporter
+        # - the bugtask pillar security contacts (if set)
+        # - the person changing the state
+        # - and bug/pillar owners, drivers if they are already subscribed
+        # If the bug is for a private project, then other direct subscribers
+        # should be unsubscribed.
+
+        (bug, bug_owner,  bugtask_a, bugtask_b, default_bugtask) = (
+            self.createBugTasksAndSubscribers())
+        initial_subscribers = set(
+            (self.factory.makePerson(), bugtask_a.owner, bug_owner,
+                bugtask_a.pillar.security_contact, bugtask_a.pillar.driver))
+
+        with person_logged_in(bug_owner):
+            for subscriber in initial_subscribers:
+                bug.subscribe(subscriber, bug_owner)
+            who = self.factory.makePerson()
+            bug.setPrivacyAndSecurityRelated(
+                private=True, security_related=True, who=who)
+            subscribers = bug.getDirectSubscribers()
+        expected_subscribers = set((
+            bugtask_a.pillar.security_contact,
+            bugtask_a.pillar.bug_supervisor,
+            bugtask_a.pillar.driver,
+            bugtask_b.pillar.security_contact,
+            bugtask_a.owner,
+            default_bugtask.pillar.owner,
+            default_bugtask.pillar.bug_supervisor,
+            bug_owner, bugtask_b.pillar.owner, who))
+        if not self.private_project:
+            expected_subscribers.update(initial_subscribers)
+        self.assertContentEqual(expected_subscribers, subscribers)
+
+    def test_setPrivateTrueAndSecurityRelatedFalse(self):
+        # When a bug is marked as private=true and security_related=false, the
+        # direct subscribers should include:
+        # - the bug reporter
+        # - the bugtask pillar bug supervisors (if set)
+        # - the person changing the state
+        # - and bug/pillar owners, drivers if they are already subscribed
+        # If the bug is for a private project, then other direct subscribers
+        # should be unsubscribed.
+
+        (bug, bug_owner,  bugtask_a, bugtask_b, default_bugtask) = (
+            self.createBugTasksAndSubscribers(private_security_related=True))
+        initial_subscribers = set(
+            (self.factory.makePerson(), bug_owner,
+                bugtask_a.pillar.security_contact, bugtask_a.pillar.driver))
+
+        with person_logged_in(bug_owner):
+            for subscriber in initial_subscribers:
+                bug.subscribe(subscriber, bug_owner)
+            who = self.factory.makePerson()
+            bug.setPrivacyAndSecurityRelated(
+                private=True, security_related=False, who=who)
+            subscribers = bug.getDirectSubscribers()
+        expected_subscribers = set((
+            bugtask_a.pillar.bug_supervisor,
+            bugtask_a.pillar.driver,
+            bugtask_b.pillar.owner,
+            default_bugtask.pillar.owner,
+            default_bugtask.pillar.bug_supervisor,
+            bug_owner, who))
+        if not self.private_project:
+            expected_subscribers.update(initial_subscribers)
+            expected_subscribers.remove(bugtask_a.pillar.security_contact)
+        self.assertContentEqual(expected_subscribers, subscribers)
+
+    def test_setPrivateFalseAndSecurityRelatedTrue(self):
+        # When a bug is marked as private=false and security_related=true, the
+        # direct subscribers should include:
+        # - the bug reporter
+        # - the bugtask pillar security contacts (if set)
+        # - and bug/pillar owners, drivers if they are already subscribed
+        # If the bug is for a private project, then other direct subscribers
+        # should be unsubscribed.
+
+        (bug, bug_owner,  bugtask_a, bugtask_b, default_bugtask) = (
+            self.createBugTasksAndSubscribers(private_security_related=True))
+        initial_subscribers = set(
+            (self.factory.makePerson(),  bug_owner,
+                bugtask_a.pillar.security_contact, bugtask_a.pillar.driver,
+                bugtask_a.pillar.bug_supervisor))
+
+        with person_logged_in(bug_owner):
+            for subscriber in initial_subscribers:
+                bug.subscribe(subscriber, bug_owner)
+            who = self.factory.makePerson()
+            bug.setPrivacyAndSecurityRelated(
+                private=False, security_related=True, who=who)
+            subscribers = bug.getDirectSubscribers()
+        expected_subscribers = set((
+            bugtask_a.pillar.security_contact,
+            bugtask_a.pillar.driver,
+            bugtask_b.pillar.security_contact,
+            default_bugtask.pillar.owner,
+            bug_owner))
+        if not self.private_project:
+            expected_subscribers.update(initial_subscribers)
+            expected_subscribers.remove(bugtask_a.pillar.bug_supervisor)
+        self.assertContentEqual(expected_subscribers, subscribers)
+
+    def test_setPrivateFalseAndSecurityRelatedFalse(self):
+        # When a bug is marked as private=false and security_related=false,
+        # any existing subscriptions are left alone.
+
+        (bug, bug_owner,  bugtask_a, bugtask_b, default_bugtask) = (
+            self.createBugTasksAndSubscribers(private_security_related=True))
+        initial_subscribers = set(
+            (self.factory.makePerson(), bug_owner,
+                bugtask_a.pillar.security_contact, bugtask_a.pillar.driver))
+
+        with person_logged_in(bug_owner):
+            for subscriber in initial_subscribers:
+                bug.subscribe(subscriber, bug_owner)
+            who = self.factory.makePerson()
+            expected_direct_subscribers = set(bug.getDirectSubscribers())
+            bug.setPrivacyAndSecurityRelated(
+                private=False, security_related=False, who=who)
+        subscribers = set(bug.getDirectSubscribers())
+        expected_direct_subscribers.remove(bugtask_a.pillar.security_contact)
+        self.assertContentEqual(expected_direct_subscribers, subscribers)
+
+    def test_setPillarOwnerSubscribedIfNoBugSupervisor(self):
+        # The pillar owner is subscribed if the bug supervisor is not set.
+
+        bug_owner = self.factory.makePerson(name='bugowner')
+        bug = self.factory.makeBug(owner=bug_owner)
+        with person_logged_in(bug_owner):
+            who = self.factory.makePerson()
+            bug.setPrivacyAndSecurityRelated(
+                private=True, security_related=False, who=who)
+            subscribers = bug.getDirectSubscribers()
+        naked_bugtask = removeSecurityProxy(bug.default_bugtask)
+        self.assertContentEqual(
+            set((naked_bugtask.pillar.owner, bug_owner, who)),
+            subscribers
+        )
+
+    def test_setPillarOwnerSubscribedIfNoSecurityContact(self):
+        # The pillar owner is subscribed if the security contact is not set.
+
+        bug_owner = self.factory.makePerson(name='bugowner')
+        bug = self.factory.makeBug(owner=bug_owner)
+        with person_logged_in(bug_owner):
+            who = self.factory.makePerson()
+            bug.setPrivacyAndSecurityRelated(
+                private=False, security_related=True, who=who)
+            subscribers = bug.getDirectSubscribers()
+        naked_bugtask = removeSecurityProxy(bug.default_bugtask)
+        self.assertContentEqual(
+            set((naked_bugtask.pillar.owner, bug_owner)),
+            subscribers
+        )
+
+    def _fetch_notifications(self, bug, header, body):
+        return IStore(BugNotification).find(
+            BugNotification,
+            BugNotification.id == BugNotificationRecipient.bug_notificationID,
+            BugNotificationRecipient.reason_header == header,
+            BugNotificationRecipient.reason_body == body,
+            BugNotification.bug == bug)
+
+
+class TestBugPrivateAndSecurityRelatedUpdatesPrivateProject(
+        TestBugPrivateAndSecurityRelatedUpdatesMixin, TestCaseWithFactory):
+
+    def setUp(self):
+        s = super(TestBugPrivateAndSecurityRelatedUpdatesPrivateProject, self)
+        s.setUp()
+        self.private_project = True
+
+
+class TestBugPrivateAndSecurityRelatedUpdatesPublicProject(
+        TestBugPrivateAndSecurityRelatedUpdatesMixin, TestCaseWithFactory):
+
+    def setUp(self):
+        s = super(TestBugPrivateAndSecurityRelatedUpdatesPublicProject, self)
+        s.setUp()
+        self.private_project = False
+
+
 class TestBugAutoConfirmation(TestCaseWithFactory):
     """Tests for auto confirming bugs"""
 

=== modified file 'lib/lp/bugs/model/tests/test_bugsummary.py'
--- lib/lp/bugs/model/tests/test_bugsummary.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/model/tests/test_bugsummary.py	2011-09-22 02:45:32 +0000
@@ -245,11 +245,11 @@
                 3 - count)
 
     def test_makePrivate(self):
-        product = self.factory.makeProduct()
-        bug = self.factory.makeBug(product=product)
-
         person_a = self.factory.makePerson()
         person_b = self.factory.makePerson()
+        product = self.factory.makeProduct()
+        bug = self.factory.makeBug(product=product, owner=person_b)
+
         bug.subscribe(person=person_a, subscribed_by=person_a)
 
         # Make the bug private. We have to use the Python API to ensure
@@ -266,7 +266,7 @@
             1)
         self.assertEqual(
             self.getCount(person_b, BugSummary.product == product),
-            0)
+            1)
         # Confirm implicit subscriptions work too.
         self.assertEqual(
             self.getCount(bug.owner, BugSummary.product == product),

=== modified file 'lib/lp/bugs/scripts/bugimport.py'
--- lib/lp/bugs/scripts/bugimport.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/scripts/bugimport.py	2011-09-22 02:45:32 +0000
@@ -328,9 +328,9 @@
             self.createAttachments(bug, msg, commentnode)
 
         # set up bug
-        bug.setPrivate(get_value(bugnode, 'private') == 'True', owner)
-        bug.setSecurityRelated(
-            get_value(bugnode, 'security_related') == 'True')
+        private = get_value(bugnode, 'private') == 'True'
+        security_related = get_value(bugnode, 'security_related') == 'True'
+        bug.setPrivacyAndSecurityRelated(private, security_related, owner)
         bug.name = get_value(bugnode, 'nickname')
         description = get_value(bugnode, 'description')
         if description:

=== modified file 'lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt'
--- lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt	2011-09-22 02:45:32 +0000
@@ -33,11 +33,12 @@
   http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2
 
 
-All the implicit subscribers have been made explicit.
+Subscribers have been updated according to the privacy rules.
 
   >>> browser.open(
   ...     "http://launchpad.dev/bugs/2/+bug-portlet-subscribers-details";)
   >>> print_direct_subscribers(browser.contents)
+  Mark Shuttleworth (Unsubscribe)
   Sample Person (Unsubscribe)
   Steve Alexander (Unsubscribe)
   Ubuntu Team (Unsubscribe)

=== modified file 'lib/lp/bugs/subscribers/bug.py'
--- lib/lp/bugs/subscribers/bug.py	2011-08-15 13:59:05 +0000
+++ lib/lp/bugs/subscribers/bug.py	2011-09-22 02:45:32 +0000
@@ -45,14 +45,6 @@
     Subscribe the security contacts for a bug when it becomes
     security-related, and add notifications for the changes.
     """
-    if (event.object.security_related and
-        not event.object_before_modification.security_related):
-        # The bug turned out to be security-related, subscribe the security
-        # contact.
-        for pillar in bug.affected_pillars:
-            if pillar.security_contact is not None:
-                bug.subscribe(pillar.security_contact, IPerson(event.user))
-
     bug_delta = get_bug_delta(
         old_bug=event.object_before_modification,
         new_bug=event.object, user=IPerson(event.user))

=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py	2011-09-19 08:35:03 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py	2011-09-22 02:45:32 +0000
@@ -18,7 +18,6 @@
 from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.testing.layers import LaunchpadFunctionalLayer
 from lp.bugs.enum import BugNotificationLevel
-from lp.bugs.interfaces.bug import IBug
 from lp.bugs.interfaces.bugtask import (
     BugTaskImportance,
     BugTaskStatus,
@@ -549,10 +548,7 @@
     def test_make_private(self):
         # Marking a bug as private adds items to the bug's activity log
         # and notifications.
-        bug_before_modification = Snapshot(self.bug, providing=IBug)
         self.bug.setPrivate(True, self.user)
-        notify(ObjectModifiedEvent(
-            self.bug, bug_before_modification, ['private'], self.user))
 
         visibility_change_activity = {
             'person': self.user,
@@ -577,10 +573,7 @@
         self.saveOldChanges(private_bug)
         self.assertTrue(private_bug.private)
 
-        bug_before_modification = Snapshot(private_bug, providing=IBug)
         private_bug.setPrivate(False, self.user)
-        notify(ObjectModifiedEvent(
-            private_bug, bug_before_modification, ['private'], self.user))
 
         visibility_change_activity = {
             'person': self.user,
@@ -648,7 +641,7 @@
     def test_mark_as_security_vulnerability(self):
         # Marking a bug as a security vulnerability adds to the bug's
         # activity log and sends a notification.
-        self.bug.setSecurityRelated(False)
+        self.bug.setSecurityRelated(False, self.user)
         self.changeAttribute(self.bug, 'security_related', True)
 
         security_change_activity = {
@@ -672,7 +665,8 @@
     def test_unmark_as_security_vulnerability(self):
         # Unmarking a bug as a security vulnerability adds to the
         # bug's activity log and sends a notification.
-        self.bug.setSecurityRelated(True)
+        self.bug.setSecurityRelated(True, self.user)
+        self.saveOldChanges()
         self.changeAttribute(self.bug, 'security_related', False)
 
         security_change_activity = {

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-09-21 19:10:33 +0000
+++ lib/lp/testing/factory.py	2011-09-22 02:45:32 +0000
@@ -1627,8 +1627,8 @@
         return branch.createBranchRevision(sequence, revision)
 
     def makeBug(self, product=None, owner=None, bug_watch_url=None,
-                private=False, date_closed=None, title=None,
-                date_created=None, description=None, comment=None,
+                private=False, security_related=False, date_closed=None,
+                title=None, date_created=None, description=None, comment=None,
                 status=None, distribution=None, milestone=None, series=None,
                 tags=None, sourcepackagename=None):
         """Create and return a new, arbitrary Bug.
@@ -1679,6 +1679,7 @@
                 sourcepackagename=sourcepackagename)
         create_bug_params = CreateBugParams(
             owner, title, comment=comment, private=private,
+            security_related=security_related,
             datecreated=date_created, description=description,
             status=status, tags=tags)
         create_bug_params.setBugTarget(