Merge lp:~wallyworld/launchpad/unshare-team-members-subscriptions2-1009358 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 15423
Proposed branch: lp:~wallyworld/launchpad/unshare-team-members-subscriptions2-1009358
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/bug-privacy-filter-storm-expressions
Diff against target: 649 lines (+259/-84)
10 files modified
lib/lp/bugs/model/bug.py (+1/-1)
lib/lp/bugs/model/bugtask.py (+1/-1)
lib/lp/bugs/model/bugtasksearch.py (+25/-13)
lib/lp/registry/interfaces/sharingjob.py (+1/-1)
lib/lp/registry/model/sharingjob.py (+37/-42)
lib/lp/registry/model/teammembership.py (+3/-4)
lib/lp/registry/services/sharingservice.py (+10/-6)
lib/lp/registry/services/tests/test_sharingservice.py (+108/-10)
lib/lp/registry/tests/test_sharingjob.py (+72/-4)
lib/lp/registry/tests/test_teammembership.py (+1/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/unshare-team-members-subscriptions2-1009358
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+110215@code.launchpad.net

Commit message

Ensure team members are unsubscribed from bugs they can no longer see when they are removed from a team.

Description of the change

== Implementation ==

This branch stops using RemoveGranteeSubscriptionsJob and replaces it with an enhanced version of RemoveBugSubscriptionsJob. This ensures the correct functionality for all cases where subscriptions need to be removed for invisible bugs. A side effect is that for now, branch subscriptions aren't removed in some cases, but this functionality was only partially implemented anyway. The next step is to introduce a RemoveBranchSubscriptionsJob to handle branches.

It was discovered doing this work that a bug existed in the sharingservice deletePillarSharee method. This was fixed and a test enhancement done to test for the issue.

A subsequent branch will delete RemoveGranteeSubscriptionsJob.

== Tests ==

Add new tests for RemoveBugSubscriptionsJob:
- test_unsubscribe_pillar_artifacts_specific_info_types
- test_admins_retain_subscriptions

Update sharing service deletePillarSharee tests to ensure the identified defect is fixed.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/model/bugtask.py
  lib/lp/bugs/model/bugtasksearch.py
  lib/lp/registry/interfaces/sharingjob.py
  lib/lp/registry/model/sharingjob.py
  lib/lp/registry/model/teammembership.py
  lib/lp/registry/services/sharingservice.py
  lib/lp/registry/services/tests/test_sharingservice.py
  lib/lp/registry/tests/test_sharingjob.py
  lib/lp/registry/tests/test_teammembership.py

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

As discussed on IRC, this branch makes the jobs completely unsuitable for product. Most notably, the jobs in a lot of cases end up verifying the subscription consistency of hundreds of thousands or millions of unrelated bugs. However, it's a good intermediate step, since the jobs remain disabled on production.

Since it doesn't make a huge amount of sense to review the functionality, there's just a couple of style comments. In addition to the below, I also propose http://pastebin.ubuntu.com/1040252/ as a bit of a cleanup around the admin special case.

185 bug_ids = [
186 - bug.id for bug in bugs
187 + bug.id for bug in bugs or []
188 + ]
189 + information_types = [
190 + info_type.value for info_type in information_types or []
191 ]

Can't those list comprehensions be on one line each?

202 + @property
203 + def information_types(self):
204 + return [
205 + enumerated_type_registry[InformationType.name].items[value]
206 + for value in self.metadata['information_types']]

Why does this use the registry? You are looking the enum up by the name you got from the enum :)

495 +# def test_revokeAccessGrantsBranches(self):
496 +# # Users with launchpad.Edit can delete all access for a sharee.
497 +# owner = self.factory.makePerson()
498 +# product = self.factory.makeProduct(owner=owner)
499 +# login_person(owner)
500 +# branch = self.factory.makeBranch(
501 +# product=product, owner=owner,
502 +# information_type=InformationType.USERDATA)
503 +# self._assert_revokeTeamAccessGrants(distro, [bug], None)

I'm not sure a commented test is useful, particularly when we have the work planned and the test won't ever work as written.

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

Thanks for the review!

On Thu 14 Jun 2012 14:45:23 EST, William Grant wrote:
> Review: Approve code
>
> As discussed on IRC, this branch makes the jobs completely unsuitable for product. Most notably, the jobs in a lot of cases end up verifying the subscription consistency of hundreds of thousands or millions of unrelated bugs. However, it's a good intermediate step, since the jobs remain disabled on production.
>

I'll add it pillar filtering prior to landing.

> Since it doesn't make a huge amount of sense to review the functionality, there's just a couple of style comments. In addition to the below, I also propose http://pastebin.ubuntu.com/1040252/ as a bit of a cleanup around the admin special case.

Looks good I think.

>
> 185 bug_ids = [
> 186 - bug.id for bug in bugs
> 187 + bug.id for bug in bugs or []
> 188 + ]
> 189 + information_types = [
> 190 + info_type.value for info_type in information_types or []
> 191 ]
>
> Can't those list comprehensions be on one line each?
>

The first one can. The second won't fit. To me it looked better if they
were both done the same way. But I've changed it.

> 202 + @property
> 203 + def information_types(self):
> 204 + return [
> 205 + enumerated_type_registry[InformationType.name].items[value]
> 206 + for value in self.metadata['information_types']]
>
> Why does this use the registry? You are looking the enum up by the name you got from the enum :)
>

I think you misread it :-) It's pulling serialised values (the names)
from the stored json and looking them up in an items list associated
with the InformationType.name attribute.

> 495 +# def test_revokeAccessGrantsBranches(self):
>
> I'm not sure a commented test is useful, particularly when we have the work planned and the test won't ever work as written.

The test worked fine in the previous branch. I had left it in as a
reminder so it could be adapted when the next job was added but will
remove it.

Revision history for this message
Ian Booth (wallyworld) wrote :

>
>> 202 + @property
>> 203 + def information_types(self):
>> 204 + return [
>> 205 + enumerated_type_registry[InformationType.name].items[value]
>> 206 + for value in self.metadata['information_types']]
>>
>> Why does this use the registry? You are looking the enum up by the name you got from the enum :)
>>
>
> I think you misread it :-) It's pulling serialised values (the names)
> from the stored json and looking them up in an items list associated
> with the InformationType.name attribute.
>

My mistake, I had grabbed some de-serialisation code and the registry
lookuo was indeed surperfulous here.

Revision history for this message
Ian Booth (wallyworld) wrote :

I've now added pillar filtering to the search. Another branch will add filtering on grantee for team membership removal.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2012-06-14 07:55:25 +0000
+++ lib/lp/bugs/model/bug.py 2012-06-14 07:55:25 +0000
@@ -1811,7 +1811,7 @@
1811 # As a result of the transition, some subscribers may no longer1811 # As a result of the transition, some subscribers may no longer
1812 # have access to the bug. We need to run a job to remove any such1812 # have access to the bug. We need to run a job to remove any such
1813 # subscriptions.1813 # subscriptions.
1814 getUtility(IRemoveBugSubscriptionsJobSource).create([self], who)1814 getUtility(IRemoveBugSubscriptionsJobSource).create(who, [self])
18151815
1816 return True1816 return True
18171817
18181818
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2012-06-14 07:55:25 +0000
+++ lib/lp/bugs/model/bugtask.py 2012-06-14 07:55:25 +0000
@@ -1193,7 +1193,7 @@
1193 # have access to the parent bug. We need to run a job to remove any1193 # have access to the parent bug. We need to run a job to remove any
1194 # such subscriptions.1194 # such subscriptions.
1195 getUtility(IRemoveBugSubscriptionsJobSource).create(1195 getUtility(IRemoveBugSubscriptionsJobSource).create(
1196 [self.bug], user)1196 user, [self.bug], pillar=target_before_change)
11971197
1198 def updateTargetNameCache(self, newtarget=None):1198 def updateTargetNameCache(self, newtarget=None):
1199 """See `IBugTask`."""1199 """See `IBugTask`."""
12001200
=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py 2012-06-14 07:55:25 +0000
+++ lib/lp/bugs/model/bugtasksearch.py 2012-06-14 07:55:25 +0000
@@ -5,6 +5,7 @@
55
6__all__ = [6__all__ = [
7 'get_bug_privacy_filter',7 'get_bug_privacy_filter',
8 'get_bug_privacy_filter_terms',
8 'orderby_expression',9 'orderby_expression',
9 'search_bugs',10 'search_bugs',
10 ]11 ]
@@ -68,6 +69,7 @@
68from lp.registry.interfaces.milestone import IProjectGroupMilestone69from lp.registry.interfaces.milestone import IProjectGroupMilestone
69from lp.registry.interfaces.product import IProduct70from lp.registry.interfaces.product import IProduct
70from lp.registry.interfaces.productseries import IProductSeries71from lp.registry.interfaces.productseries import IProductSeries
72from lp.registry.interfaces.role import IPersonRoles
71from lp.registry.model.accesspolicy import AccessPolicyGrant73from lp.registry.model.accesspolicy import AccessPolicyGrant
72from lp.registry.model.distribution import Distribution74from lp.registry.model.distribution import Distribution
73from lp.registry.model.milestone import Milestone75from lp.registry.model.milestone import Milestone
@@ -1378,9 +1380,15 @@
1378 :return: A SQL filter, a decorator to cache visibility in a resultset that1380 :return: A SQL filter, a decorator to cache visibility in a resultset that
1379 returns BugTask objects.1381 returns BugTask objects.
1380 """1382 """
1381 bug_filter_terms = _get_bug_privacy_filter_terms(user)1383 # Admins can see all bugs, so we can short-circuit the filter.
1382 if not bug_filter_terms:1384 if user is not None and IPersonRoles(user).in_admin:
1383 return True, _nocache_bug_decorator1385 return True, _nocache_bug_decorator
1386
1387 # We want an actual Storm Person.
1388 if IPersonRoles.providedBy(user):
1389 user = user.person
1390
1391 bug_filter_terms = get_bug_privacy_filter_terms(user, check_admin=False)
1384 if len(bug_filter_terms) == 1:1392 if len(bug_filter_terms) == 1:
1385 return bug_filter_terms[0], _nocache_bug_decorator1393 return bug_filter_terms[0], _nocache_bug_decorator
13861394
@@ -1388,24 +1396,19 @@
1388 return expr, _make_cache_user_can_view_bug(user)1396 return expr, _make_cache_user_can_view_bug(user)
13891397
13901398
1391def _get_bug_privacy_filter_terms(user):1399def get_bug_privacy_filter_terms(user, check_admin=True):
1392 public_bug_filter = (1400 public_bug_filter = (
1393 BugTaskFlat.information_type.is_in(PUBLIC_INFORMATION_TYPES))1401 BugTaskFlat.information_type.is_in(PUBLIC_INFORMATION_TYPES))
13941402
1395 if user is None:1403 if user is None:
1396 return [public_bug_filter]1404 return [public_bug_filter]
13971405
1398 admin_team = getUtility(ILaunchpadCelebrities).admin
1399 if removeSecurityProxy(user).inTeam(admin_team):
1400 return []
1401
1402 artifact_grant_query = Coalesce(1406 artifact_grant_query = Coalesce(
1403 ArrayIntersects(SQL('BugTaskFlat.access_grants'),1407 ArrayIntersects(SQL('BugTaskFlat.access_grants'),
1404 Select(1408 Select(
1405 ArrayAgg(TeamParticipation.teamID),1409 ArrayAgg(TeamParticipation.teamID),
1406 tables=TeamParticipation,1410 tables=TeamParticipation,
1407 where=(TeamParticipation.personID ==1411 where=(TeamParticipation.person == user)
1408 user.id)
1409 )), False)1412 )), False)
14101413
1411 policy_grant_query = Coalesce(1414 policy_grant_query = Coalesce(
@@ -1416,9 +1419,18 @@
1416 Join(TeamParticipation,1419 Join(TeamParticipation,
1417 TeamParticipation.teamID ==1420 TeamParticipation.teamID ==
1418 AccessPolicyGrant.grantee_id)),1421 AccessPolicyGrant.grantee_id)),
1419 where=(1422 where=(TeamParticipation.person == user)
1420 TeamParticipation.personID ==
1421 user.id)
1422 )), False)1423 )), False)
14231424
1424 return [public_bug_filter, artifact_grant_query, policy_grant_query]1425 filters = [public_bug_filter, artifact_grant_query, policy_grant_query]
1426
1427 if check_admin:
1428 filters.append(
1429 Exists(Select(
1430 1, tables=[TeamParticipation],
1431 where=And(
1432 TeamParticipation.person == user,
1433 TeamParticipation.team ==
1434 getUtility(ILaunchpadCelebrities).admin))))
1435
1436 return filters
14251437
=== modified file 'lib/lp/registry/interfaces/sharingjob.py'
--- lib/lp/registry/interfaces/sharingjob.py 2012-05-22 12:05:51 +0000
+++ lib/lp/registry/interfaces/sharingjob.py 2012-06-14 07:55:25 +0000
@@ -92,7 +92,7 @@
92class IRemoveBugSubscriptionsJobSource(ISharingJobSource):92class IRemoveBugSubscriptionsJobSource(ISharingJobSource):
93 """An interface for acquiring IRemoveBugSubscriptionsJobs."""93 """An interface for acquiring IRemoveBugSubscriptionsJobs."""
9494
95 def create(pillar, bugs, requestor):95 def create(pillar, requestor, bugs=None, information_types=None):
96 """Create a new job to remove subscriptions for the specified bugs.96 """Create a new job to remove subscriptions for the specified bugs.
9797
98 Subscriptions for users who no longer have access to the bugs are98 Subscriptions for users who no longer have access to the bugs are
9999
=== modified file 'lib/lp/registry/model/sharingjob.py'
--- lib/lp/registry/model/sharingjob.py 2012-06-14 07:55:25 +0000
+++ lib/lp/registry/model/sharingjob.py 2012-06-14 07:55:25 +0000
@@ -19,18 +19,15 @@
19from lazr.enum import (19from lazr.enum import (
20 DBEnumeratedType,20 DBEnumeratedType,
21 DBItem,21 DBItem,
22 enumerated_type_registry,
23 )22 )
24import simplejson23import simplejson
25from sqlobject import SQLObjectNotFound24from sqlobject import SQLObjectNotFound
26from storm.expr import (25from storm.expr import (
27 And,26 And,
28 Coalesce,
29 In,27 In,
30 Join,
31 Not,28 Not,
29 Or,
32 Select,30 Select,
33 SQL,
34 )31 )
35from storm.locals import (32from storm.locals import (
36 Int,33 Int,
@@ -48,7 +45,10 @@
48from lp.bugs.model.bug import Bug45from lp.bugs.model.bug import Bug
49from lp.bugs.model.bugsubscription import BugSubscription46from lp.bugs.model.bugsubscription import BugSubscription
50from lp.bugs.model.bugtaskflat import BugTaskFlat47from lp.bugs.model.bugtaskflat import BugTaskFlat
51from lp.bugs.model.bugtasksearch import get_bug_privacy_filter48from lp.bugs.model.bugtasksearch import (
49 get_bug_privacy_filter,
50 get_bug_privacy_filter_terms,
51 )
52from lp.code.interfaces.branchlookup import IBranchLookup52from lp.code.interfaces.branchlookup import IBranchLookup
53from lp.registry.enums import InformationType53from lp.registry.enums import InformationType
54from lp.registry.interfaces.person import IPersonSet54from lp.registry.interfaces.person import IPersonSet
@@ -61,19 +61,13 @@
61 ISharingJob,61 ISharingJob,
62 ISharingJobSource,62 ISharingJobSource,
63 )63 )
64from lp.registry.model.accesspolicy import AccessPolicyGrant
65from lp.registry.model.distribution import Distribution64from lp.registry.model.distribution import Distribution
66from lp.registry.model.person import Person65from lp.registry.model.person import Person
67from lp.registry.model.product import Product66from lp.registry.model.product import Product
68from lp.registry.model.teammembership import TeamParticipation
69from lp.services.config import config67from lp.services.config import config
70from lp.services.database.enumcol import EnumCol68from lp.services.database.enumcol import EnumCol
71from lp.services.database.lpstorm import IStore69from lp.services.database.lpstorm import IStore
72from lp.services.database.stormbase import StormBase70from lp.services.database.stormbase import StormBase
73from lp.services.database.stormexpr import (
74 ArrayAgg,
75 ArrayIntersects,
76 )
77from lp.services.job.model.job import (71from lp.services.job.model.job import (
78 EnumeratedSubclass,72 EnumeratedSubclass,
79 Job,73 Job,
@@ -307,7 +301,7 @@
307 @property301 @property
308 def information_types(self):302 def information_types(self):
309 return [303 return [
310 enumerated_type_registry[InformationType.name].items[value]304 InformationType.items[value]
311 for value in self.metadata['information_types']]305 for value in self.metadata['information_types']]
312306
313 def getErrorRecipients(self):307 def getErrorRecipients(self):
@@ -402,18 +396,21 @@
402 config = config.IRemoveBugSubscriptionsJobSource396 config = config.IRemoveBugSubscriptionsJobSource
403397
404 @classmethod398 @classmethod
405 def create(cls, bugs, requestor):399 def create(cls, requestor, bugs=None, grantee=None, pillar=None,
400 information_types=None):
406 """See `IRemoveBugSubscriptionsJob`."""401 """See `IRemoveBugSubscriptionsJob`."""
407402
408 bug_ids = [403 bug_ids = [bug.id for bug in bugs or []]
409 bug.id for bug in bugs404 information_types = [
405 info_type.value for info_type in information_types or []
410 ]406 ]
411 metadata = {407 metadata = {
412 'bug_ids': bug_ids,408 'bug_ids': bug_ids,
409 'information_types': information_types,
413 'requestor.id': requestor.id410 'requestor.id': requestor.id
414 }411 }
415 return super(RemoveBugSubscriptionsJob, cls).create(412 return super(RemoveBugSubscriptionsJob, cls).create(
416 None, None, metadata)413 pillar, grantee, metadata)
417414
418 @property415 @property
419 def requestor_id(self):416 def requestor_id(self):
@@ -431,6 +428,12 @@
431 def bugs(self):428 def bugs(self):
432 return getUtility(IBugSet).getByNumbers(self.bug_ids)429 return getUtility(IBugSet).getByNumbers(self.bug_ids)
433430
431 @property
432 def information_types(self):
433 return [
434 InformationType.items[value]
435 for value in self.metadata['information_types']]
436
434 def getErrorRecipients(self):437 def getErrorRecipients(self):
435 # If something goes wrong we want to let the requestor know as well438 # If something goes wrong we want to let the requestor know as well
436 # as the pillar maintainer (if there is a pillar).439 # as the pillar maintainer (if there is a pillar).
@@ -453,35 +456,27 @@
453456
454 # Find all bug subscriptions for which the subscriber cannot see the457 # Find all bug subscriptions for which the subscriber cannot see the
455 # bug.458 # bug.
456 constraints = [459 invisible_filter = (
457 BugTaskFlat.bug_id.is_in(self.bug_ids),460 Not(Or(*get_bug_privacy_filter_terms(BugSubscription.person_id))))
458 Not(Coalesce(461 filters = [invisible_filter]
459 ArrayIntersects(SQL('BugTaskFlat.access_grants'),462
460 Select(463 if self.bug_ids:
461 ArrayAgg(TeamParticipation.teamID),464 filters.append(BugTaskFlat.bug_id.is_in(self.bug_ids))
462 tables=TeamParticipation,465 else:
463 where=(TeamParticipation.personID ==466 if self.information_types:
464 BugSubscription.person_id)467 filters.append(
465 )), False)),468 BugTaskFlat.information_type.is_in(self.information_types))
466 Not(Coalesce(469 if self.product:
467 ArrayIntersects(SQL('BugTaskFlat.access_policies'),470 filters.append(
468 Select(471 BugTaskFlat.product == self.product)
469 ArrayAgg(AccessPolicyGrant.policy_id),472 if self.distro:
470 tables=(AccessPolicyGrant,473 filters.append(
471 Join(TeamParticipation,474 BugTaskFlat.distribution == self.distro)
472 TeamParticipation.teamID ==475
473 AccessPolicyGrant.grantee_id)),
474 where=(
475 TeamParticipation.personID ==
476 BugSubscription.person_id)
477 )), False))
478 ]
479 subscriptions = IStore(BugSubscription).find(476 subscriptions = IStore(BugSubscription).find(
480 BugSubscription,477 BugSubscription,
481 In(BugSubscription.bug_id,478 In(BugSubscription.bug_id,
482 Select(479 Select(BugTaskFlat.bug_id, where=And(*filters)))
483 BugTaskFlat.bug_id,
484 where=And(*constraints)))
485 )480 )
486 for sub in subscriptions:481 for sub in subscriptions:
487 sub.bug.unsubscribe(482 sub.bug.unsubscribe(
488483
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2012-06-14 07:55:25 +0000
+++ lib/lp/registry/model/teammembership.py 2012-06-14 07:55:25 +0000
@@ -43,7 +43,7 @@
43 )43 )
44from lp.registry.interfaces.role import IPersonRoles44from lp.registry.interfaces.role import IPersonRoles
45from lp.registry.interfaces.sharingjob import (45from lp.registry.interfaces.sharingjob import (
46 IRemoveGranteeSubscriptionsJobSource,46 IRemoveBugSubscriptionsJobSource,
47 )47 )
48from lp.registry.interfaces.teammembership import (48from lp.registry.interfaces.teammembership import (
49 ACTIVE_STATES,49 ACTIVE_STATES,
@@ -393,9 +393,8 @@
393 # A person has left the team so they may no longer have access393 # A person has left the team so they may no longer have access
394 # to some artifacts shared with the team. We need to run a job394 # to some artifacts shared with the team. We need to run a job
395 # to remove any subscriptions to such artifacts.395 # to remove any subscriptions to such artifacts.
396 getUtility(IRemoveGranteeSubscriptionsJobSource).create(396 getUtility(IRemoveBugSubscriptionsJobSource).create(
397 None, self.person, user)397 user, grantee=self.team)
398
399 else:398 else:
400 # Changed from an inactive state to another inactive one, so no399 # Changed from an inactive state to another inactive one, so no
401 # need to fill/clean the TeamParticipation table.400 # need to fill/clean the TeamParticipation table.
402401
=== modified file 'lib/lp/registry/services/sharingservice.py'
--- lib/lp/registry/services/sharingservice.py 2012-06-14 07:55:25 +0000
+++ lib/lp/registry/services/sharingservice.py 2012-06-14 07:55:25 +0000
@@ -40,7 +40,7 @@
40from lp.registry.interfaces.product import IProduct40from lp.registry.interfaces.product import IProduct
41from lp.registry.interfaces.projectgroup import IProjectGroup41from lp.registry.interfaces.projectgroup import IProjectGroup
42from lp.registry.interfaces.sharingjob import (42from lp.registry.interfaces.sharingjob import (
43 IRemoveGranteeSubscriptionsJobSource,43 IRemoveBugSubscriptionsJobSource,
44 )44 )
45from lp.registry.interfaces.sharingservice import ISharingService45from lp.registry.interfaces.sharingservice import ISharingService
46from lp.registry.model.person import Person46from lp.registry.model.person import Person
@@ -328,12 +328,13 @@
328 if len(to_delete) > 0:328 if len(to_delete) > 0:
329 accessartifact_grant_source = getUtility(329 accessartifact_grant_source = getUtility(
330 IAccessArtifactGrantSource)330 IAccessArtifactGrantSource)
331 accessartifact_grant_source.revokeByArtifact(to_delete)331 accessartifact_grant_source.revokeByArtifact(to_delete, [sharee])
332332
333 # Create a job to remove subscriptions for artifacts the sharee can no333 # Create a job to remove subscriptions for artifacts the sharee can no
334 # longer see.334 # longer see.
335 getUtility(IRemoveGranteeSubscriptionsJobSource).create(335 getUtility(IRemoveBugSubscriptionsJobSource).create(
336 pillar, sharee, user, information_types=information_types)336 user, bugs=None, pillar=pillar,
337 information_types=information_types)
337338
338 @available_with_permission('launchpad.Edit', 'pillar')339 @available_with_permission('launchpad.Edit', 'pillar')
339 def revokeAccessGrants(self, pillar, sharee, user, branches=None,340 def revokeAccessGrants(self, pillar, sharee, user, branches=None,
@@ -358,8 +359,11 @@
358359
359 # Create a job to remove subscriptions for artifacts the sharee can no360 # Create a job to remove subscriptions for artifacts the sharee can no
360 # longer see.361 # longer see.
361 getUtility(IRemoveGranteeSubscriptionsJobSource).create(362 if bugs:
362 pillar, sharee, user, bugs=bugs, branches=branches)363 getUtility(IRemoveBugSubscriptionsJobSource).create(
364 user, bugs, pillar=pillar)
365 # XXX 2012-06-13 wallyworld bug=1012448
366 # Remove branch subscriptions when information type fully implemented.
363367
364 def ensureAccessGrants(self, sharee, user, branches=None, bugs=None,368 def ensureAccessGrants(self, sharee, user, branches=None, bugs=None,
365 ignore_permissions=False):369 ignore_permissions=False):
366370
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2012-06-14 07:55:25 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-06-14 07:55:25 +0000
@@ -26,10 +26,12 @@
26 )26 )
27from lp.registry.interfaces.accesspolicy import (27from lp.registry.interfaces.accesspolicy import (
28 IAccessArtifactGrantSource,28 IAccessArtifactGrantSource,
29 IAccessArtifactSource,
29 IAccessPolicyGrantFlatSource,30 IAccessPolicyGrantFlatSource,
30 IAccessPolicyGrantSource,31 IAccessPolicyGrantSource,
31 IAccessPolicySource,32 IAccessPolicySource,
32 )33 )
34from lp.registry.interfaces.person import TeamSubscriptionPolicy
33from lp.registry.services.sharingservice import SharingService35from lp.registry.services.sharingservice import SharingService
34from lp.services.features.testing import FeatureFixture36from lp.services.features.testing import FeatureFixture
35from lp.services.job.tests import block_on_job37from lp.services.job.tests import block_on_job
@@ -55,7 +57,7 @@
55WRITE_FLAG = {57WRITE_FLAG = {
56 'disclosure.enhanced_sharing.writable': 'true',58 'disclosure.enhanced_sharing.writable': 'true',
57 'disclosure.enhanced_sharing_details.enabled': 'true',59 'disclosure.enhanced_sharing_details.enabled': 'true',
58 'jobs.celery.enabled_classes': 'RemoveGranteeSubscriptionsJob'}60 'jobs.celery.enabled_classes': 'RemoveBugSubscriptionsJob'}
59DETAILS_FLAG = {'disclosure.enhanced_sharing_details.enabled': 'true'}61DETAILS_FLAG = {'disclosure.enhanced_sharing_details.enabled': 'true'}
6062
6163
@@ -541,12 +543,15 @@
541 # Make some artifact grants for our sharee.543 # Make some artifact grants for our sharee.
542 artifact = self.factory.makeAccessArtifact()544 artifact = self.factory.makeAccessArtifact()
543 self.factory.makeAccessArtifactGrant(artifact, grantee)545 self.factory.makeAccessArtifactGrant(artifact, grantee)
546 # Make some access policy grants for another sharee.
547 another = self.factory.makePerson()
548 self.factory.makeAccessPolicyGrant(access_policies[0], another)
549 # Make some artifact grants for our yet another sharee.
550 yet_another = self.factory.makePerson()
551 self.factory.makeAccessArtifactGrant(artifact, yet_another)
544 for access_policy in access_policies:552 for access_policy in access_policies:
545 self.factory.makeAccessPolicyArtifact(553 self.factory.makeAccessPolicyArtifact(
546 artifact=artifact, policy=access_policy)554 artifact=artifact, policy=access_policy)
547 # Make some access policy grants for another sharee.
548 another = self.factory.makePerson()
549 self.factory.makeAccessPolicyGrant(access_policies[0], another)
550 # Delete data for a specific information type.555 # Delete data for a specific information type.
551 with FeatureFixture(WRITE_FLAG):556 with FeatureFixture(WRITE_FLAG):
552 self.service.deletePillarSharee(557 self.service.deletePillarSharee(
@@ -563,10 +568,16 @@
563 expected_data = [568 expected_data = [
564 (grantee, {policy: SharingPermission.ALL}, [])569 (grantee, {policy: SharingPermission.ALL}, [])
565 for policy in expected_policies]570 for policy in expected_policies]
566 # Add the expected data for the other sharee.571 # Add the expected data for the other sharees.
567 another_person_data = (572 another_person_data = (
568 another, {access_policies[0]: SharingPermission.ALL}, [])573 another, {access_policies[0]: SharingPermission.ALL}, [])
569 expected_data.append(another_person_data)574 expected_data.append(another_person_data)
575 policy_permissions = dict([(
576 policy, SharingPermission.SOME) for policy in access_policies])
577 yet_another_person_data = (
578 yet_another, policy_permissions,
579 [InformationType.USERDATA, InformationType.EMBARGOEDSECURITY])
580 expected_data.append(yet_another_person_data)
570 self.assertContentEqual(581 self.assertContentEqual(
571 expected_data, self.service.getPillarSharees(pillar))582 expected_data, self.service.getPillarSharees(pillar))
572583
@@ -768,15 +779,102 @@
768 information_type=InformationType.USERDATA)779 information_type=InformationType.USERDATA)
769 self._assert_revokeAccessGrants(distro, [bug], None)780 self._assert_revokeAccessGrants(distro, [bug], None)
770781
771 def test_revokeAccessGrantsBranches(self):782 # XXX 2012-06-13 wallyworld bug=1012448
783 # Remove branch subscriptions when information type fully implemented.
784# def test_revokeAccessGrantsBranches(self):
785# # Users with launchpad.Edit can delete all access for a sharee.
786# owner = self.factory.makePerson()
787# product = self.factory.makeProduct(owner=owner)
788# login_person(owner)
789# branch = self.factory.makeBranch(
790# product=product, owner=owner,
791# information_type=InformationType.USERDATA)
792# self._assert_revokeAccessGrants(product, None, [branch])
793
794 def _assert_revokeTeamAccessGrants(self, pillar, bugs, branches):
795 artifacts = []
796 if bugs:
797 artifacts.extend(bugs)
798 if branches:
799 artifacts.extend(branches)
800 policy = self.factory.makeAccessPolicy(pillar=pillar)
801
802 person_grantee = self.factory.makePerson()
803 team_owner = self.factory.makePerson()
804 team_grantee = self.factory.makeTeam(
805 owner=team_owner,
806 subscription_policy=TeamSubscriptionPolicy.RESTRICTED,
807 members=[person_grantee])
808
809 # Subscribe the team and person grantees to the artifacts.
810 for person in [team_grantee, person_grantee]:
811 for bug in bugs or []:
812 bug.subscribe(person, pillar.owner)
813 # XXX 2012-06-12 wallyworld bug=1002596
814 # No need to revoke AAG with triggers removed.
815 if person == person_grantee:
816 accessartifact_source = getUtility(IAccessArtifactSource)
817 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
818 accessartifact_source.find([bug]), [person_grantee])
819 for branch in branches or []:
820 branch.subscribe(person,
821 BranchSubscriptionNotificationLevel.NOEMAIL, None,
822 CodeReviewNotificationLevel.NOEMAIL, pillar.owner)
823
824 # Check that grantees have expected access grants and subscriptions.
825 for person in [team_grantee, person_grantee]:
826 visible_bugs, visible_branches = self.service.getVisibleArtifacts(
827 person, branches, bugs)
828 self.assertContentEqual(bugs or [], visible_bugs)
829 self.assertContentEqual(branches or [], visible_branches)
830 for person in [team_grantee, person_grantee]:
831 for bug in bugs or []:
832 self.assertIn(person, bug.getDirectSubscribers())
833
834 with FeatureFixture(WRITE_FLAG):
835 self.service.revokeAccessGrants(
836 pillar, team_grantee, pillar.owner,
837 bugs=bugs, branches=branches)
838 with block_on_job(self):
839 transaction.commit()
840
841 # The grantees now have no access to anything.
842 apgfs = getUtility(IAccessPolicyGrantFlatSource)
843 permission_info = apgfs.findGranteePermissionsByPolicy(
844 [policy], [team_grantee, person_grantee])
845 self.assertEqual(0, permission_info.count())
846
847 # Check that the grantee's subscriptions have been removed.
848 # Branches will be done once they have the information_type attribute.
849 for person in [team_grantee, person_grantee]:
850 for bug in bugs or []:
851 self.assertNotIn(person, bug.getDirectSubscribers())
852 visible_bugs, visible_branches = self.service.getVisibleArtifacts(
853 person, branches, bugs)
854 self.assertContentEqual([], visible_bugs)
855 self.assertContentEqual([], visible_branches)
856
857 def test_revokeTeamAccessGrantsBugs(self):
772 # Users with launchpad.Edit can delete all access for a sharee.858 # Users with launchpad.Edit can delete all access for a sharee.
773 owner = self.factory.makePerson()859 owner = self.factory.makePerson()
774 product = self.factory.makeProduct(owner=owner)860 distro = self.factory.makeDistribution(owner=owner)
775 login_person(owner)861 login_person(owner)
776 branch = self.factory.makeBranch(862 bug = self.factory.makeBug(
777 product=product, owner=owner,863 distribution=distro, owner=owner,
778 information_type=InformationType.USERDATA)864 information_type=InformationType.USERDATA)
779 self._assert_revokeAccessGrants(product, None, [branch])865 self._assert_revokeTeamAccessGrants(distro, [bug], None)
866
867 # XXX 2012-06-13 wallyworld bug=1012448
868 # Remove branch subscriptions when information type fully implemented.
869# def test_revokeAccessGrantsBranches(self):
870# # Users with launchpad.Edit can delete all access for a sharee.
871# owner = self.factory.makePerson()
872# product = self.factory.makeProduct(owner=owner)
873# login_person(owner)
874# branch = self.factory.makeBranch(
875# product=product, owner=owner,
876# information_type=InformationType.USERDATA)
877# self._assert_revokeTeamAccessGrants(distro, [bug], None)
780878
781 def _assert_revokeAccessGrantsUnauthorized(self):879 def _assert_revokeAccessGrantsUnauthorized(self):
782 # revokeAccessGrants raises an Unauthorized exception if the user880 # revokeAccessGrants raises an Unauthorized exception if the user
783881
=== modified file 'lib/lp/registry/tests/test_sharingjob.py'
--- lib/lp/registry/tests/test_sharingjob.py 2012-06-14 07:55:25 +0000
+++ lib/lp/registry/tests/test_sharingjob.py 2012-06-14 07:55:25 +0000
@@ -481,7 +481,7 @@
481481
482 def create_job(distro, bug, grantee, owner):482 def create_job(distro, bug, grantee, owner):
483 job = getUtility(IRemoveBugSubscriptionsJobSource).create(483 job = getUtility(IRemoveBugSubscriptionsJobSource).create(
484 [bug], owner)484 owner, [bug])
485 with person_logged_in(owner):485 with person_logged_in(owner):
486 bug.transitionToInformationType(486 bug.transitionToInformationType(
487 InformationType.EMBARGOEDSECURITY, owner)487 InformationType.EMBARGOEDSECURITY, owner)
@@ -515,7 +515,7 @@
515 requestor = self.factory.makePerson()515 requestor = self.factory.makePerson()
516 bug = self.factory.makeBug()516 bug = self.factory.makeBug()
517 job = getUtility(IRemoveBugSubscriptionsJobSource).create(517 job = getUtility(IRemoveBugSubscriptionsJobSource).create(
518 [bug], requestor)518 requestor, [bug])
519 naked_job = removeSecurityProxy(job)519 naked_job = removeSecurityProxy(job)
520 self.assertIsInstance(job, RemoveBugSubscriptionsJob)520 self.assertIsInstance(job, RemoveBugSubscriptionsJob)
521 self.assertEqual(requestor.id, naked_job.requestor_id)521 self.assertEqual(requestor.id, naked_job.requestor_id)
@@ -527,7 +527,7 @@
527 product = self.factory.makeProduct()527 product = self.factory.makeProduct()
528 bug = self.factory.makeBug(product=product)528 bug = self.factory.makeBug(product=product)
529 job = getUtility(IRemoveBugSubscriptionsJobSource).create(529 job = getUtility(IRemoveBugSubscriptionsJobSource).create(
530 [bug], requestor)530 requestor, [bug], pillar=product)
531 expected_emails = [531 expected_emails = [
532 format_address_for_person(person)532 format_address_for_person(person)
533 for person in (product.owner, requestor)]533 for person in (product.owner, requestor)]
@@ -580,7 +580,7 @@
580 reconcile_access_for_artifact(580 reconcile_access_for_artifact(
581 bug, bug.information_type, bug.affected_pillars)581 bug, bug.information_type, bug.affected_pillars)
582582
583 getUtility(IRemoveBugSubscriptionsJobSource).create([bug], owner)583 getUtility(IRemoveBugSubscriptionsJobSource).create(owner, [bug])
584 with block_on_job(self):584 with block_on_job(self):
585 transaction.commit()585 transaction.commit()
586586
@@ -611,3 +611,71 @@
611 removeSecurityProxy(bug).default_bugtask.product = another_product611 removeSecurityProxy(bug).default_bugtask.product = another_product
612612
613 self._assert_bug_change_unsubscribes(change_target)613 self._assert_bug_change_unsubscribes(change_target)
614
615 def _make_subscribed_bug(self, grantee, product=None, distribution=None,
616 information_type=InformationType.USERDATA):
617 owner = self.factory.makePerson()
618 bug = self.factory.makeBug(
619 owner=owner, product=product, distribution=distribution,
620 information_type=information_type)
621 with person_logged_in(owner):
622 bug.subscribe(grantee, owner)
623 # Subscribing grantee to bug creates an access grant so we need to
624 # revoke that for our test.
625 accessartifact_source = getUtility(IAccessArtifactSource)
626 accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
627 accessartifact_grant_source.revokeByArtifact(
628 accessartifact_source.find([bug]), [grantee])
629
630 return bug, owner
631
632 def test_unsubscribe_pillar_artifacts_specific_info_types(self):
633 # Only remove subscriptions for bugs of the specified info type.
634
635 person_grantee = self.factory.makePerson(name='grantee')
636
637 owner = self.factory.makePerson(name='pillarowner')
638 pillar = self.factory.makeProduct(owner=owner)
639
640 # Make bugs the person_grantee is subscribed to.
641 bug1, ignored = self._make_subscribed_bug(
642 person_grantee, product=pillar,
643 information_type=InformationType.USERDATA)
644
645 bug2, ignored = self._make_subscribed_bug(
646 person_grantee, product=pillar,
647 information_type=InformationType.EMBARGOEDSECURITY)
648
649 # Now run the job, removing access to userdata artifacts.
650 getUtility(IRemoveBugSubscriptionsJobSource).create(
651 pillar.owner, pillar=pillar,
652 information_types=[InformationType.USERDATA])
653 with block_on_job(self):
654 transaction.commit()
655
656 self.assertNotIn(
657 person_grantee, removeSecurityProxy(bug1).getDirectSubscribers())
658 self.assertIn(
659 person_grantee, removeSecurityProxy(bug2).getDirectSubscribers())
660
661 def test_admins_retain_subscriptions(self):
662 # Admins subscriptions are retained even if they don't have explicit
663 # access.
664 product = self.factory.makeProduct()
665 owner = self.factory.makePerson()
666 admin = getUtility(ILaunchpadCelebrities).admin.teamowner
667
668 login_person(owner)
669 bug = self.factory.makeBug(
670 owner=owner, product=product,
671 information_type=InformationType.USERDATA)
672
673 bug.subscribe(admin, owner)
674 getUtility(IRemoveBugSubscriptionsJobSource).create(
675 owner, [bug], pillar=product)
676 with block_on_job(self):
677 transaction.commit()
678
679 # Check the result. admin should still be subscribed.
680 subscribers = removeSecurityProxy(bug).getDirectSubscribers()
681 self.assertIn(admin, subscribers)
614682
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2012-06-14 07:55:25 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2012-06-14 07:55:25 +0000
@@ -999,8 +999,7 @@
999 def setUp(self):999 def setUp(self):
1000 self.useFixture(FeatureFixture({1000 self.useFixture(FeatureFixture({
1001 'disclosure.unsubscribe_jobs.enabled': 'true',1001 'disclosure.unsubscribe_jobs.enabled': 'true',
1002 'jobs.celery.enabled_classes':1002 'jobs.celery.enabled_classes': 'RemoveBugSubscriptionsJob',
1003 'RemoveGranteeSubscriptionsJob',
1004 }))1003 }))
1005 super(TestTeamMembershipJobs, self).setUp()1004 super(TestTeamMembershipJobs, self).setUp()
10061005