Merge lp:~stevenk/launchpad/structsub-private-bugs-redux into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 15720
Proposed branch: lp:~stevenk/launchpad/structsub-private-bugs-redux
Merge into: lp:launchpad
Diff against target: 840 lines (+173/-263)
16 files modified
database/schema/security.cfg (+2/-0)
lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt (+3/-18)
lib/lp/bugs/doc/bug.txt (+5/-12)
lib/lp/bugs/doc/bugsubscription.txt (+5/-16)
lib/lp/bugs/interfaces/bug.py (+1/-4)
lib/lp/bugs/mail/bugnotificationrecipients.py (+1/-1)
lib/lp/bugs/model/bug.py (+54/-51)
lib/lp/bugs/model/structuralsubscription.py (+8/-0)
lib/lp/bugs/model/tests/test_bug.py (+0/-19)
lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py (+1/-40)
lib/lp/bugs/scripts/bugnotification.py (+1/-2)
lib/lp/bugs/subscribers/bug.py (+7/-10)
lib/lp/bugs/tests/test_bug_notification_recipients.py (+75/-0)
lib/lp/bugs/tests/test_bugchanges.py (+0/-78)
lib/lp/registry/model/accesspolicy.py (+1/-1)
lib/lp/registry/tests/test_sharingjob.py (+9/-11)
To merge this branch: bzr merge lp:~stevenk/launchpad/structsub-private-bugs-redux
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+116798@code.launchpad.net

Commit message

Filter direct (in one case), indirect, structural and duplicate subscriptions on bugs by those who have an AAG or APG.

Description of the change

Filter indirect, structural and duplicate subscriptions by those who have an AAG or APG. Remove include_master_dupe_subscribers for being pointless and unused.

I have removed a whole swatch of tests for being completely pointless with this change.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

Looking very good, Steve. I have many comments, as always, including a couple of bugs.

152 -The upstream maintainer will be subscribed to security-related private
153 -bugs, because upstream has no security contact, in this case.

This branch is a prerequisite for the work that will make this test obsolete, but I believe it's still relevant. It's talking about an actual BugSubscription here.

170 === modified file 'lib/lp/bugs/doc/security-teams.txt'

Likewise with both changes here. This branch only enables implicit subscriptions -- we still need to test that explicit subscriptions work until we remove them.

201 - def getBugNotificationRecipients(duplicateof=None, old_bug=None,
202 - include_master_dupe_subscribers=False):
203 + def getBugNotificationRecipients(duplicateof=None, old_bug=None):

Nice cleanup of a dead codepath.

278 + def forbidden_recipients_filter(self, column=None):

This actually filters to the allowed recipients, not the forbidden ones. I also wonder if it would be better to return a single expression, which is the Or or True, letting callsites just include it inline without extend()ing. And is it really worth having column= be optional?

337 + all = Select(BugTask.assigneeID, BugTask.bug == self.bug)
338 + assignees = load_people(Person.id.is_in(all))

Why have you split the subquery out into a separate local? Might as well inline it, or at least not shadow a builtin. Also, did you consider inlining the subsequent filter into load_people's where argument?

368 + return subscribers.difference(
369 + self.direct_subscribers_at_all_levels,

Direct subscribers will usually have a grant, but not always (eg. after their project access has been revoked, but the sub removal job hasn't run yet). This needs to be filtered too.

392 + if bug.private:
393 + filters.extend([
394 + Or(*get_bug_privacy_filter_terms(
395 + StructuralSubscription.subscriberID)),
396 + BugTaskFlat.bug == bug])

I'm a little concerned about this. The function didn't previously return nothing for private bugs, so this may be the wrong level to filter at. Did you consider other locations?

424 - def test_structural_bug_supervisor_becomes_direct_on_private(self):
425 - # If a bug supervisor has a structural subscription to the bug, and
426 - # the bug is marked as private, the supervisor should get a direct
427 - # subscription. Otherwise they should be removed, per other tests.

Why is this test no longer valid? Yes, the behaviour should be removed later, but this branch doesn't do it.

448 - def test_information_type_does_not_leak(self):
449 - # Make sure that bug notifications for private bugs do not leak to
450 - # people with a subscription on the product.

This seems like a reasonable integration test to retain. You've added a test that sharing => notification in test_private_bug_with_structural_subscriber_with_access, but unless I've missed one this is the only one that tests !sharing => !notification.

797 - def test_private_bug_target_change_doesnt_add_everyone(self):

Again, I don't see that this test's value is gone.

review: Needs Fixing (code)
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2012-07-25 18:56:53 +0000
3+++ database/schema/security.cfg 2012-07-31 01:59:19 +0000
4@@ -1513,8 +1513,10 @@
5 [bugnotification]
6 groups=script
7 public.accessartifact = SELECT, INSERT, DELETE
8+public.accessartifactgrant = SELECT
9 public.accesspolicy = SELECT
10 public.accesspolicyartifact = SELECT, INSERT, DELETE
11+public.accesspolicygrant = SELECT
12 public.account = SELECT
13 public.answercontact = SELECT
14 public.archive = SELECT
15
16=== modified file 'lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt'
17--- lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt 2012-07-27 01:15:04 +0000
18+++ lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt 2012-07-31 01:59:19 +0000
19@@ -387,17 +387,6 @@
20 >>> filebug_view.added_bug.information_type
21 <DBItem InformationType.USERDATA, (4) Private>
22
23-Since the bug was marked private before it was filed, only the bug
24-reporter has been subscribed to the bug and there should be no indirect
25-subscribers.
26-
27- >>> for subscriber in filebug_view.added_bug.getDirectSubscribers():
28- ... print subscriber.displayname
29- No Privileges Person
30-
31- >>> filebug_view.added_bug.getIndirectSubscribers()
32- []
33-
34 The user will be notified that the bug has been marked as private.
35
36 >>> print filebug_view.request.response.notifications[2].message
37@@ -594,11 +583,6 @@
38 Mark Shuttleworth
39 No Privileges Person
40
41-Since the bug is private, there should be no indirect subscribers.
42-
43- >>> filebug_view.added_bug.getIndirectSubscribers()
44- []
45-
46 The user will be notified that Mark Shuttleworth has been subscribed to
47 this bug and that the bug has been marked as private.
48
49@@ -718,8 +702,9 @@
50 >>> product.setBugSupervisor(owner, owner)
51 >>> supervisor_fields = set(filebug_view.field_names)
52
53-Privileged users get most of the same fields as normal users, plus a few extra.
54-The security_related checkbox is replaced by an information_type radio group.
55+Privileged users get most of the same fields as normal users, plus a few
56+extra. The security_related checkbox is replaced by an information_type
57+radio group.
58
59 >>> normal_fields.remove('security_related')
60 >>> owner_fields == supervisor_fields
61
62=== modified file 'lib/lp/bugs/doc/bug.txt'
63--- lib/lp/bugs/doc/bug.txt 2012-07-19 04:40:03 +0000
64+++ lib/lp/bugs/doc/bug.txt 2012-07-31 01:59:19 +0000
65@@ -169,7 +169,7 @@
66 private. A bug cannot be made private by an anonymous user.
67
68 >>> from lp.services.webapp.interfaces import ILaunchBag
69-
70+
71 >>> def current_user():
72 ... return getUtility(ILaunchBag).user
73
74@@ -423,11 +423,6 @@
75 >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()]
76 [u'name16']
77
78-Since it's private, there are no indirect subscribers.
79-
80- >>> private_bug.getIndirectSubscribers()
81- []
82-
83 It's up to the submitter to subscribe the maintainer, if they so choose.
84
85 This works similarly for distributions; in this case the
86@@ -449,8 +444,6 @@
87 >>> private_bug = bugset.get(added_bug.id)
88 >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()]
89 [u'name16']
90- >>> private_bug.getIndirectSubscribers()
91- []
92
93
94 Prevent reporter from being subscribed to filed bugs
95@@ -1084,10 +1077,10 @@
96 The thunderbird project does not use Launchpad to track bugs.
97 Incomplete, unattended bug reports cannot ever expire for this project.
98
99- >>> # create_old_bug creates an bug with a bugtask that is eligible for
100- >>> # expiration, so long as the pillar object has enabled bug expiration.
101- >>> # Every change to a bug or bugtask must be synced back to the
102- >>> # database to test can_expire.
103+create_old_bug creates an bug with a bugtask that is eligible for expiration,
104+so long as the pillar object has enabled bug expiration. Every change to a
105+bug or bugtask must be synced back to the database to test can_expire.
106+
107 >>> from lp.bugs.tests.bug import create_old_bug
108
109 >>> upstream_bugtask = create_old_bug('bug a', 1, thunderbird)
110
111=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
112--- lib/lp/bugs/doc/bugsubscription.txt 2012-07-27 01:15:04 +0000
113+++ lib/lp/bugs/doc/bugsubscription.txt 2012-07-31 01:59:19 +0000
114@@ -400,18 +400,6 @@
115 Mark Shuttleworth
116 Robert Collins
117
118-A private bug never has indirect subscribers. Let's add an indirect subscriber
119-to show that they still aren't included in the indirect subscriptions.
120-
121- >>> linux_source_bug.bugtasks[0].transitionToAssignee(
122- ... personset.getByName("martin-pitt"))
123-
124- >>> linux_source_bug.getIndirectSubscribers()
125- []
126-
127- >>> linux_source_bug.getSubscribersFromDuplicates()
128- ()
129-
130 Direct subscriptions always take precedence over indirect subscriptions.
131 So, if we unmark the above bug as private, indirect_subscribers will include
132 any subscribers not converted to direct subscribers.
133@@ -425,14 +413,12 @@
134 Robert Collins
135
136 >>> print_displayname(linux_source_bug.getIndirectSubscribers())
137- Martin Pitt
138 No Privileges Person
139 Sample Person
140 Scott James Remnant
141 Ubuntu Team
142
143 >>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
144- Martin Pitt
145 No Privileges Person
146 Sample Person
147 Scott James Remnant
148@@ -786,8 +772,11 @@
149 ... owner=foobar, information_type=InformationType.PRIVATESECURITY)
150 >>> new_bug = firefox.createBug(params)
151
152- >>> getSubscribers(new_bug)
153- ['foo.bar@canonical.com', 'test@canonical.com']
154+ >>> print '\n'.join(getSubscribers(new_bug))
155+ foo.bar@canonical.com
156+ mark@example.com
157+ robertc@robertcollins.net
158+ test@canonical.com
159
160 Now let's create a bug on a specific package, which has no package bug
161 contacts:
162
163=== modified file 'lib/lp/bugs/interfaces/bug.py'
164--- lib/lp/bugs/interfaces/bug.py 2012-07-30 00:05:44 +0000
165+++ lib/lp/bugs/interfaces/bug.py 2012-07-31 01:59:19 +0000
166@@ -518,8 +518,7 @@
167 `BugSubscriptionLevel.LIFECYCLE` if unspecified.
168 """
169
170- def getBugNotificationRecipients(duplicateof=None, old_bug=None,
171- include_master_dupe_subscribers=False):
172+ def getBugNotificationRecipients(duplicateof=None, old_bug=None):
173 """Return a complete INotificationRecipientSet instance.
174
175 The INotificationRecipientSet instance will contain details of
176@@ -527,8 +526,6 @@
177 includes email addresses and textual and header-ready
178 rationales. See `BugNotificationRecipients` for
179 details of this implementation.
180- If this bug is a dupe, set include_master_dupe_subscribers to
181- True to include the master bug's subscribers as recipients.
182 """
183
184 def canBeAQuestion():
185
186=== modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py'
187--- lib/lp/bugs/mail/bugnotificationrecipients.py 2012-07-27 01:36:30 +0000
188+++ lib/lp/bugs/mail/bugnotificationrecipients.py 2012-07-31 01:59:19 +0000
189@@ -56,7 +56,7 @@
190 duplicateof parameter above and the addDupeSubscriber method.
191 Don't confuse them!
192 """
193- NotificationRecipientSet.__init__(self)
194+ super(BugNotificationRecipients, self).__init__()
195 self.duplicateof = duplicateof
196 self.subscription_filters = set()
197
198
199=== modified file 'lib/lp/bugs/model/bug.py'
200--- lib/lp/bugs/model/bug.py 2012-07-30 00:05:44 +0000
201+++ lib/lp/bugs/model/bug.py 2012-07-31 01:59:19 +0000
202@@ -166,8 +166,6 @@
203 from lp.registry.interfaces.accesspolicy import (
204 IAccessArtifactGrantSource,
205 IAccessArtifactSource,
206- IAccessPolicyGrantSource,
207- IAccessPolicySource,
208 )
209 from lp.registry.interfaces.distribution import IDistribution
210 from lp.registry.interfaces.distroseries import IDistroSeries
211@@ -979,7 +977,8 @@
212 """See `IBug`."""
213 return self.getSubscriptionInfo().direct_subscriptions
214
215- def getDirectSubscribers(self, recipients=None, level=None):
216+ def getDirectSubscribers(self, recipients=None, level=None,
217+ filter_visible=False):
218 """See `IBug`.
219
220 The recipients argument is private and not exposed in the
221@@ -991,6 +990,13 @@
222 level = BugNotificationLevel.LIFECYCLE
223 direct_subscribers = (
224 self.getSubscriptionInfo(level).direct_subscribers)
225+ if filter_visible:
226+ filtered_subscribers = IStore(Person).find(Person,
227+ Person.id.is_in([s.id for s in direct_subscribers]),
228+ self.getSubscriptionInfo().visible_recipients_filter(
229+ Person.id))
230+ direct_subscribers = BugSubscriberSet(
231+ direct_subscribers.intersection(filtered_subscribers))
232 if recipients is not None:
233 for subscriber in direct_subscribers:
234 recipients.addDirectSubscriber(subscriber)
235@@ -1123,27 +1129,13 @@
236 return get_also_notified_subscribers(self, recipients, level)
237
238 def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,
239- level=None,
240- include_master_dupe_subscribers=False):
241+ level=None):
242 """See `IBug`."""
243 recipients = BugNotificationRecipients(duplicateof=duplicateof)
244- self.getDirectSubscribers(recipients, level=level)
245- if self.private:
246- assert self.getIndirectSubscribers() == [], (
247- "Indirect subscribers found on private bug. "
248- "A private bug should never have implicit subscribers!")
249- else:
250- self.getIndirectSubscribers(recipients, level=level)
251- if include_master_dupe_subscribers and self.duplicateof:
252- # This bug is a public duplicate of another bug, so include
253- # the dupe target's subscribers in the recipient list. Note
254- # that we only do this for duplicate bugs that are public;
255- # changes in private bugs are not broadcast to their dupe
256- # targets.
257- dupe_recipients = (
258- self.duplicateof.getBugNotificationRecipients(
259- duplicateof=self.duplicateof, level=level))
260- recipients.update(dupe_recipients)
261+ self.getDirectSubscribers(
262+ recipients, level=level, filter_visible=True)
263+ self.getIndirectSubscribers(recipients, level=level)
264+
265 # XXX Tom Berger 2008-03-18:
266 # We want to look up the recipients for `old_bug` too,
267 # but for this to work, this code has to move out of the
268@@ -2241,9 +2233,6 @@
269 else:
270 raise ValueError('First argument must be bug or bugtask')
271
272- if bug.private:
273- return []
274-
275 # Subscribers to exclude.
276 exclude_subscribers = frozenset().union(
277 info.direct_subscribers_at_all_levels, info.muted_subscribers)
278@@ -2452,6 +2441,17 @@
279 muted_people = Select(BugMute.person_id, BugMute.bug == self.bug)
280 return load_people(Person.id.is_in(muted_people))
281
282+ def visible_recipients_filter(self, column):
283+ # Circular fail :(
284+ from lp.bugs.model.bugtaskflat import BugTaskFlat
285+ from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms
286+
287+ if self.bug.private:
288+ return [Or(*get_bug_privacy_filter_terms(column)),
289+ BugTaskFlat.bug_id == self.bug.id]
290+ else:
291+ return True
292+
293 @cachedproperty
294 @freeze(BugSubscriptionSet)
295 def direct_subscriptions(self):
296@@ -2496,21 +2496,20 @@
297 def duplicate_subscriptions(self):
298 """Subscriptions to duplicates of the bug.
299
300- Excludes muted subscriptions.
301+ Excludes muted subscriptions, and subscribers who can not see the
302+ master bug.
303 """
304- if self.bug.private:
305- return ()
306- else:
307- return IStore(BugSubscription).find(
308- BugSubscription,
309- BugSubscription.bug_notification_level >= self.level,
310- BugSubscription.bug_id == Bug.id,
311- Bug.duplicateof == self.bug,
312- Not(In(
313- BugSubscription.person_id,
314- Select(
315- BugMute.person_id, BugMute.bug_id == Bug.id,
316- tables=[BugMute]))))
317+ return IStore(BugSubscription).find(
318+ BugSubscription,
319+ BugSubscription.bug_notification_level >= self.level,
320+ BugSubscription.bug_id == Bug.id,
321+ Bug.duplicateof == self.bug,
322+ Not(In(
323+ BugSubscription.person_id,
324+ Select(
325+ BugMute.person_id, BugMute.bug_id == Bug.id,
326+ tables=[BugMute]))),
327+ self.visible_recipients_filter(BugSubscription.person_id))
328
329 @property
330 def duplicate_subscribers(self):
331@@ -2571,22 +2570,26 @@
332 *Does not* exclude muted subscribers.
333 """
334 if self.bugtask is None:
335- assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug)
336- return load_people(Person.id.is_in(assignees))
337- else:
338- return load_people(Person.id == self.bugtask.assigneeID)
339+ assignees = load_people(
340+ Person.id.is_in(Select(BugTask.assigneeID,
341+ BugTask.bug == self.bug)))
342+ else:
343+ assignees = load_people(Person.id == self.bugtask.assigneeID)
344+ if self.bug.private:
345+ return IStore(Person).find(Person,
346+ Person.id.is_in([a.id for a in assignees]),
347+ self.visible_recipients_filter(Person.id))
348+ else:
349+ return assignees
350
351 @cachedproperty
352 def also_notified_subscribers(self):
353 """All subscribers except direct, dupe, and muted subscribers."""
354- if self.bug.private:
355- return BugSubscriberSet()
356- else:
357- subscribers = BugSubscriberSet().union(
358- self.structural_subscribers, self.all_assignees)
359- return subscribers.difference(
360- self.direct_subscribers_at_all_levels,
361- self.muted_subscribers)
362+ subscribers = BugSubscriberSet().union(
363+ self.structural_subscribers, self.all_assignees)
364+ return subscribers.difference(
365+ self.direct_subscribers_at_all_levels,
366+ self.muted_subscribers)
367
368 @cachedproperty
369 def indirect_subscribers(self):
370
371=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
372--- lib/lp/bugs/model/structuralsubscription.py 2012-06-14 05:18:22 +0000
373+++ lib/lp/bugs/model/structuralsubscription.py 2012-07-31 01:59:19 +0000
374@@ -702,6 +702,9 @@
375 :param direct_subscribers: a collection of Person objects who are
376 directly subscribed to the bug.
377 """
378+ # Circular. :-(
379+ from lp.bugs.model.bugtaskflat import BugTaskFlat
380+ from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms
381 # We get the ids because we need to use group by in order to
382 # look at the filters' tags in aggregate. Once we have the ids,
383 # we can get the full set of what we need in subsuming or
384@@ -738,6 +741,11 @@
385 Not(In(StructuralSubscription.subscriberID,
386 Select(BugSubscription.person_id,
387 BugSubscription.bug == bug))))
388+ if bug.private:
389+ filters.extend([
390+ Or(*get_bug_privacy_filter_terms(
391+ StructuralSubscription.subscriberID)),
392+ BugTaskFlat.bug == bug])
393 candidates = list(_get_structural_subscriptions(
394 StructuralSubscription.id, query_arguments, *filters))
395 if not candidates:
396
397=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
398--- lib/lp/bugs/model/tests/test_bug.py 2012-07-26 05:18:32 +0000
399+++ lib/lp/bugs/model/tests/test_bug.py 2012-07-31 01:59:19 +0000
400@@ -891,25 +891,6 @@
401 bug.setPrivate(True, bug.owner)
402 self.assertEqual(InformationType.USERDATA, bug.information_type)
403
404- def test_information_type_does_not_leak(self):
405- # Make sure that bug notifications for private bugs do not leak to
406- # people with a subscription on the product.
407- product = self.factory.makeProduct()
408- with person_logged_in(product.owner):
409- product.addSubscription(product.owner, product.owner)
410- reporter = self.factory.makePerson()
411- bug = self.factory.makeBug(
412- information_type=InformationType.USERDATA, product=product,
413- owner=reporter)
414- recipients = Store.of(bug).using(
415- BugNotificationRecipient,
416- Join(BugNotification, BugNotification.bugID == bug.id)).find(
417- BugNotificationRecipient,
418- BugNotificationRecipient.bug_notificationID ==
419- BugNotification.id)
420- self.assertEqual(
421- [reporter], [recipient.person for recipient in recipients])
422-
423 def test__reconcileAccess_handles_all_targets(self):
424 # _reconcileAccess gets the pillar from any task
425 # type.
426
427=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py'
428--- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-07-27 01:15:04 +0000
429+++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-07-31 01:59:19 +0000
430@@ -273,18 +273,6 @@
431 self.assertContentEqual(
432 [duplicate_bug_subscription], found_subscriptions)
433
434- def test_duplicate_for_private_bug(self):
435- # The set of subscribers from duplicate bugs is always empty when the
436- # master bug is private.
437- duplicate_bug = self.factory.makeBug(product=self.target)
438- with person_logged_in(duplicate_bug.owner):
439- duplicate_bug.markAsDuplicate(self.bug)
440- with person_logged_in(self.bug.owner):
441- self.bug.setPrivate(True, self.bug.owner)
442- found_subscriptions = self.getInfo().duplicate_subscriptions
443- self.assertContentEqual([], found_subscriptions)
444- self.assertContentEqual([], found_subscriptions.subscribers)
445-
446 def test_duplicate_only(self):
447 # The set of duplicate subscriptions where the subscriber has no other
448 # subscriptions.
449@@ -430,33 +418,6 @@
450 found_subscribers = self.getInfo().also_notified_subscribers
451 self.assertContentEqual([], found_subscribers)
452
453- def test_also_notified_subscribers_for_private_bug(self):
454- # The set of also notified subscribers is always empty when the master
455- # bug is private.
456- assignee = self.factory.makePerson()
457- bugtask = self.bug.default_bugtask
458- with person_logged_in(bugtask.pillar.bug_supervisor):
459- bugtask.transitionToAssignee(assignee)
460- with person_logged_in(self.bug.owner):
461- self.bug.setPrivate(True, self.bug.owner)
462- found_subscribers = self.getInfo().also_notified_subscribers
463- self.assertContentEqual([], found_subscribers)
464-
465- def test_indirect_subscribers(self):
466- # The set of indirect subscribers is the union of also notified
467- # subscribers and subscribers to duplicates.
468- assignee = self.factory.makePerson()
469- bugtask = self.bug.default_bugtask
470- with person_logged_in(bugtask.pillar.bug_supervisor):
471- bugtask.transitionToAssignee(assignee)
472- duplicate_bug = self.factory.makeBug(product=self.target)
473- with person_logged_in(duplicate_bug.owner):
474- duplicate_bug.markAsDuplicate(self.bug)
475- found_subscribers = self.getInfo().indirect_subscribers
476- self.assertContentEqual(
477- [assignee, duplicate_bug.owner],
478- found_subscribers)
479-
480
481 class TestBugSubscriptionInfoPermissions(TestCaseWithFactory):
482
483@@ -584,7 +545,7 @@
484 self.make_duplicate_bug()
485 with person_logged_in(self.bug.owner):
486 self.bug.setPrivate(True, self.bug.owner)
487- with self.exactly_x_queries(0):
488+ with self.exactly_x_queries(1):
489 self.info.duplicate_subscriptions
490 with self.exactly_x_queries(0):
491 self.info.duplicate_subscriptions.subscribers
492
493=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
494--- lib/lp/bugs/scripts/bugnotification.py 2012-05-08 17:04:43 +0000
495+++ lib/lp/bugs/scripts/bugnotification.py 2012-07-31 01:59:19 +0000
496@@ -313,8 +313,7 @@
497 # Construct the real notification with recipients.
498 bug = notification.bug
499 recipients = bug.getBugNotificationRecipients(
500- level=BugNotificationLevel.LIFECYCLE,
501- include_master_dupe_subscribers=False)
502+ level=BugNotificationLevel.LIFECYCLE)
503 message = notification.message
504 is_comment = notification.is_comment
505 activity = notification.activity
506
507=== modified file 'lib/lp/bugs/subscribers/bug.py'
508--- lib/lp/bugs/subscribers/bug.py 2012-05-08 01:06:31 +0000
509+++ lib/lp/bugs/subscribers/bug.py 2012-07-31 01:59:19 +0000
510@@ -156,12 +156,11 @@
511 level=BugNotificationLevel.METADATA)
512 recipients.update(old_bugtask_recipients)
513 for change in changes:
514+ bug = bug_delta.bug
515 if isinstance(change, BugDuplicateChange):
516- no_dupe_master_recipients = (
517- bug_delta.bug.getBugNotificationRecipients(
518- old_bug=bug_delta.bug_before_modification,
519- level=change.change_level,
520- include_master_dupe_subscribers=False))
521+ no_dupe_master_recipients = bug.getBugNotificationRecipients(
522+ old_bug=bug_delta.bug_before_modification,
523+ level=change.change_level)
524 bug_delta.bug.addChange(
525 change, recipients=no_dupe_master_recipients)
526 elif (isinstance(change, BugTaskAssigneeChange) and
527@@ -173,11 +172,9 @@
528 bug_delta.bug.addChange(change, recipients=recipients)
529 else:
530 if change.change_level == BugNotificationLevel.LIFECYCLE:
531- change_recipients = (
532- bug_delta.bug.getBugNotificationRecipients(
533- old_bug=bug_delta.bug_before_modification,
534- level=change.change_level,
535- include_master_dupe_subscribers=False))
536+ change_recipients = bug.getBugNotificationRecipients(
537+ old_bug=bug_delta.bug_before_modification,
538+ level=change.change_level)
539 recipients.update(change_recipients)
540 # Additionally, if we are re-targetting a bugtask for a private
541 # bug, we need to ensure the new bug supervisor and maintainer are
542
543=== modified file 'lib/lp/bugs/tests/test_bug_notification_recipients.py'
544--- lib/lp/bugs/tests/test_bug_notification_recipients.py 2012-07-24 05:43:46 +0000
545+++ lib/lp/bugs/tests/test_bug_notification_recipients.py 2012-07-31 01:59:19 +0000
546@@ -5,7 +5,13 @@
547
548 __metaclass__ = type
549
550+from zope.component import getUtility
551+
552 from lp.registry.enums import InformationType
553+from lp.registry.interfaces.accesspolicy import (
554+ IAccessArtifactGrantSource,
555+ IAccessPolicySource,
556+ )
557 from lp.testing import (
558 person_logged_in,
559 TestCaseWithFactory,
560@@ -59,6 +65,7 @@
561 bug.getBugNotificationRecipients())
562
563 def test_private_bug(self):
564+ # Only the owner is notified about a private bug.
565 owner = self.factory.makePerson()
566 bug = self.factory.makeBug(
567 owner=owner, information_type=InformationType.USERDATA)
568@@ -67,6 +74,7 @@
569 [owner], bug.getBugNotificationRecipients())
570
571 def test_private_bug_with_subscriber(self):
572+ # Subscribing a user to a bug grants access, so they will be notified.
573 owner = self.factory.makePerson()
574 subscriber = self.factory.makePerson()
575 bug = self.factory.makeBug(
576@@ -76,7 +84,23 @@
577 self.assertContentEqual(
578 [owner, subscriber], bug.getBugNotificationRecipients())
579
580+ def test_private_bug_with_subscriber_without_access(self):
581+ # A subscriber without access to a private bug isn't notified.
582+ owner = self.factory.makePerson()
583+ subscriber = self.factory.makePerson()
584+ bug = self.factory.makeBug(
585+ owner=owner, information_type=InformationType.USERDATA)
586+ artifact = self.factory.makeAccessArtifact(concrete=bug)
587+ with person_logged_in(owner):
588+ bug.subscribe(subscriber, owner)
589+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
590+ [artifact], [subscriber])
591+ self.assertContentEqual(
592+ [owner], bug.getBugNotificationRecipients())
593+
594 def test_private_bug_with_structural_subscriber(self):
595+ # A structural subscriber without access does not get notified about
596+ # a private bug.
597 owner = self.factory.makePerson()
598 subscriber = self.factory.makePerson()
599 product = self.factory.makeProduct()
600@@ -89,7 +113,26 @@
601 self.assertContentEqual(
602 [owner], bug.getBugNotificationRecipients())
603
604+ def test_private_bug_with_structural_subscriber_with_access(self):
605+ # When a structural subscriber has access to a private bug, they are
606+ # notified.
607+ owner = self.factory.makePerson()
608+ subscriber = self.factory.makePerson()
609+ product = self.factory.makeProduct()
610+ with person_logged_in(subscriber):
611+ product.addBugSubscription(subscriber, subscriber)
612+ policy = getUtility(IAccessPolicySource).find(
613+ [(product, InformationType.USERDATA)]).one()
614+ self.factory.makeAccessPolicyGrant(policy=policy, grantee=subscriber)
615+ bug = self.factory.makeBug(
616+ product=product, owner=owner,
617+ information_type=InformationType.USERDATA)
618+ with person_logged_in(owner):
619+ self.assertContentEqual(
620+ [owner, subscriber], bug.getBugNotificationRecipients())
621+
622 def test_private_bug_assignee(self):
623+ # Assigning a user to a private bug does not give them visibility.
624 owner = self.factory.makePerson()
625 assignee = self.factory.makePerson()
626 bug = self.factory.makeBug(
627@@ -99,7 +142,22 @@
628 self.assertContentEqual(
629 [owner], bug.getBugNotificationRecipients())
630
631+ def test_private_bug_assignee_with_access(self):
632+ # An assignee with access will get notified.
633+ owner = self.factory.makePerson()
634+ assignee = self.factory.makePerson()
635+ bug = self.factory.makeBug(
636+ owner=owner, information_type=InformationType.USERDATA)
637+ artifact = self.factory.makeAccessArtifact(concrete=bug)
638+ self.factory.makeAccessArtifactGrant(
639+ artifact=artifact, grantee=assignee)
640+ with person_logged_in(owner):
641+ bug.default_bugtask.transitionToAssignee(assignee)
642+ self.assertContentEqual(
643+ [owner, assignee], bug.getBugNotificationRecipients())
644+
645 def test_private_bug_with_duplicate_subscriber(self):
646+ # A subscriber to a duplicate of a private bug will not be notified.
647 owner = self.factory.makePerson()
648 subscriber = self.factory.makePerson()
649 bug = self.factory.makeBug(
650@@ -110,3 +168,20 @@
651 dupe.markAsDuplicate(bug)
652 self.assertContentEqual(
653 [owner], bug.getBugNotificationRecipients())
654+
655+ def test_private_bug_with_duplicate_subscriber_with_access(self):
656+ # A subscriber to a duplicate of a private bug will be notified, if
657+ # they have access.
658+ owner = self.factory.makePerson()
659+ subscriber = self.factory.makePerson()
660+ bug = self.factory.makeBug(
661+ owner=owner, information_type=InformationType.USERDATA)
662+ artifact = self.factory.makeAccessArtifact(concrete=bug)
663+ self.factory.makeAccessArtifactGrant(
664+ artifact=artifact, grantee=subscriber)
665+ dupe = self.factory.makeBug(owner=owner)
666+ with person_logged_in(owner):
667+ dupe.subscribe(subscriber, owner)
668+ dupe.markAsDuplicate(bug)
669+ self.assertContentEqual(
670+ [owner, subscriber], bug.getBugNotificationRecipients())
671
672=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
673--- lib/lp/bugs/tests/test_bugchanges.py 2012-07-19 04:40:03 +0000
674+++ lib/lp/bugs/tests/test_bugchanges.py 2012-07-31 01:59:19 +0000
675@@ -31,7 +31,6 @@
676 from lp.services.webapp.publisher import canonical_url
677 from lp.testing import (
678 api_url,
679- celebrity_logged_in,
680 launchpadlib_for,
681 login_person,
682 person_logged_in,
683@@ -1046,83 +1045,6 @@
684 expected_activity=expected_activity,
685 expected_notification=expected_notification)
686
687- def _test_retarget_private_security_bug_to_product(self,
688- bug, maintainer,
689- bug_supervisor=None):
690- # When a private security related bug has a bugtask retargetted to a
691- # different product, a notification is sent to the new bug supervisor
692- # and maintainer. If they are the same person, only one notification
693- # is sent. They only get notifications if they can see the bug.
694-
695- # Create the private bug.
696- bug_task = bug.bugtasks[0]
697- new_target = self.factory.makeProduct(
698- owner=maintainer, bug_supervisor=bug_supervisor)
699- self.saveOldChanges(bug)
700-
701- bug_task_before_modification = Snapshot(
702- bug_task, providing=providedBy(bug_task))
703- bug_task.transitionToTarget(new_target, self.user)
704- notify(ObjectModifiedEvent(
705- bug_task, bug_task_before_modification,
706- ['target', 'product'], user=self.user))
707-
708- expected_activity = {
709- 'person': self.user,
710- 'whatchanged': 'affects',
711- 'oldvalue': bug_task_before_modification.bugtargetname,
712- 'newvalue': bug_task.bugtargetname,
713- }
714-
715- expected_recipients = [self.user]
716- expected_reasons = ['Subscriber']
717- if bug.userCanView(maintainer):
718- expected_recipients.append(maintainer)
719- expected_reasons.append('Maintainer')
720- if (bug_supervisor and not bug_supervisor.inTeam(maintainer)
721- and bug.userCanView(bug_supervisor)):
722- expected_recipients.append(bug_supervisor)
723- expected_reasons.append('Bug Supervisor')
724- expected_notification = {
725- 'text': u"** Project changed: %s => %s" % (
726- bug_task_before_modification.bugtargetname,
727- bug_task.bugtargetname),
728- 'person': self.user,
729- 'recipients': expected_recipients,
730- 'recipient_reasons': expected_reasons
731- }
732- self.assertRecordedChange(
733- expected_activity=expected_activity,
734- expected_notification=expected_notification, bug=bug)
735-
736- def test_retarget_private_security_bug_to_product(self):
737- # A series of tests for re-targetting a private bug task.
738- bug = self.factory.makeBug(
739- product=self.product, owner=self.user,
740- information_type=InformationType.USERDATA)
741- maintainer = self.factory.makePerson()
742- bug_supervisor = self.factory.makePerson()
743-
744- # Test with no bug supervisor
745- self._test_retarget_private_security_bug_to_product(bug, maintainer)
746- # Test with bug supervisor = maintainer.
747- self._test_retarget_private_security_bug_to_product(
748- bug, maintainer, maintainer)
749- # Test with different bug supervisor
750- self._test_retarget_private_security_bug_to_product(
751- bug, maintainer, bug_supervisor)
752-
753- # Now make the bug visible to the bug supervisor and re-test.
754- with celebrity_logged_in('admin'):
755- bug.default_bugtask.transitionToAssignee(bug_supervisor)
756-
757- # Test with bug supervisor = maintainer.
758- self._test_retarget_private_security_bug_to_product(
759- bug, maintainer, maintainer)
760- # Test with different bug supervisor
761- self._test_retarget_private_security_bug_to_product(
762- bug, maintainer, bug_supervisor)
763-
764 def test_target_bugtask_to_sourcepackage(self):
765 # When a bugtask's target is changed, BugActivity and
766 # BugNotification get updated.
767
768=== modified file 'lib/lp/registry/model/accesspolicy.py'
769--- lib/lp/registry/model/accesspolicy.py 2012-06-19 02:14:21 +0000
770+++ lib/lp/registry/model/accesspolicy.py 2012-07-31 01:59:19 +0000
771@@ -334,7 +334,7 @@
772
773 @classmethod
774 def revokeByArtifact(cls, artifacts, grantees=None):
775- """See `IAccessPolicyGrantSource`."""
776+ """See `IAccessArtifactGrantSource`."""
777 cls.findByArtifact(artifacts, grantees).remove()
778
779
780
781=== modified file 'lib/lp/registry/tests/test_sharingjob.py'
782--- lib/lp/registry/tests/test_sharingjob.py 2012-07-19 04:40:03 +0000
783+++ lib/lp/registry/tests/test_sharingjob.py 2012-07-31 01:59:19 +0000
784@@ -19,7 +19,6 @@
785 from lp.registry.enums import InformationType
786 from lp.registry.interfaces.accesspolicy import (
787 IAccessArtifactGrantSource,
788- IAccessArtifactSource,
789 IAccessPolicySource,
790 )
791 from lp.registry.interfaces.person import TeamSubscriptionPolicy
792@@ -182,9 +181,9 @@
793 job, job_type = create_job(distro, bug, grantee, owner)
794 # Subscribing grantee has created an artifact grant so we need to
795 # revoke that to test the job.
796+ artifact = self.factory.makeAccessArtifact(concrete=bug)
797 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
798- getUtility(IAccessArtifactSource).find(
799- [bug]), [grantee])
800+ [artifact], [grantee])
801 transaction.commit()
802
803 out, err, exit_code = run_script(
804@@ -295,9 +294,9 @@
805 CodeReviewNotificationLevel.NOEMAIL, owner)
806 # Subscribing policy_team_grantee has created an artifact grant so we
807 # need to revoke that to test the job.
808+ artifact = self.factory.makeAccessArtifact(concrete=bug)
809 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
810- getUtility(IAccessArtifactSource).find(
811- [bug]), [policy_team_grantee])
812+ [artifact], [policy_team_grantee])
813
814 # policy grantees are subscribed because the job has not been run yet.
815 subscribers = removeSecurityProxy(bug).getDirectSubscribers()
816@@ -367,9 +366,9 @@
817 bug.subscribe(artifact_indirect_grantee, owner)
818 # Subscribing policy_team_grantee has created an artifact grant so we
819 # need to revoke that to test the job.
820+ artifact = self.factory.makeAccessArtifact(concrete=branch)
821 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
822- getUtility(IAccessArtifactSource).find(
823- [branch]), [policy_team_grantee])
824+ [artifact], [policy_team_grantee])
825
826 # policy grantees are subscribed because the job has not been run yet.
827 #subscribers = removeSecurityProxy(branch).subscribers
828@@ -432,10 +431,9 @@
829 bug.subscribe(grantee, owner)
830 # Subscribing grantee to bug creates an access grant so we need to
831 # revoke that for our test.
832- accessartifact_source = getUtility(IAccessArtifactSource)
833- accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
834- accessartifact_grant_source.revokeByArtifact(
835- accessartifact_source.find([bug]), [grantee])
836+ artifact = self.factory.makeAccessArtifact(concrete=bug)
837+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
838+ [artifact], [grantee])
839
840 return bug, owner
841