Merge lp:~adeuring/launchpad/bug-1056881 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Abel Deuring
Approved revision: no longer in the source branch.
Merged at revision: 16303
Proposed branch: lp:~adeuring/launchpad/bug-1056881
Merge into: lp:launchpad
Diff against target: 395 lines (+228/-13)
6 files modified
lib/lp/blueprints/model/specification.py (+29/-0)
lib/lp/blueprints/model/specificationsubscription.py (+30/-3)
lib/lp/blueprints/model/tests/test_specification.py (+51/-3)
lib/lp/blueprints/model/tests/test_subscription.py (+61/-6)
lib/lp/blueprints/tests/test_specification.py (+56/-0)
lib/lp/blueprints/tests/test_webservice.py (+1/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-1056881
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+135374@code.launchpad.net

Commit message

Add an AccessArtifactGrant when somebody is subscribed to a non-publis specification.

Description of the change

This branch fixes bug 1056881: Specifications privacy: subscribers can't see private blueprints

The fix follows the pattern already used for private bugs and branches: When users are subscribed to a private branch or bug and if they do not have a policy grant for the pillar, an artifact grant is created.

Automatically adding grants when somebody is subscribed does not help if a public specification is made proprietary or embargoed, so I changed transitionToInformationType() also: Subscribers who would lose access to the specification get automatically an artifact grant.

Another change: SPecification owners and admins can now unsubscribe other subscribers; before, only the subscribers themselves could do this. This limitation is obviously bad when the owner of a private spec inadvertently subscribes the wrong peron or team, or when an access grant should be revoked after some time.

tests:

./bin/test blueprints -vvt lp.blueprints.model.tests.test_specification.TestSpecificationInformationType.test_transitionToInformationType_adds_grants_for_subscribers
./bin/test blueprints -vvt tests.test_specification.*subscribe
./bin/test blueprints -vvt tests.test_subscription.SpecificationTests

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/model/specification.py
  lib/lp/blueprints/model/specificationsubscription.py
  lib/lp/blueprints/model/tests/test_specification.py
  lib/lp/blueprints/model/tests/test_subscription.py
  lib/lp/blueprints/tests/test_specification.py

./lib/lp/blueprints/model/tests/test_specification.py
     625: E251 no spaces around keyword / parameter equals
     659: E251 no spaces around keyword / parameter equals

the errors indicate that we use too long names:

    624 product = self.factory.makeProduct(
    625 specification_sharing_policy=
    626 SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

This is definitely an improvement. Please ensure a bug is filed about the lack of a subscribed_by field in SpecificationSubscription, and reference it in the security adapter and test comments. Please fix the too-long lines. You can do this by assigning the enum to a local variable.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/model/specification.py'
2--- lib/lp/blueprints/model/specification.py 2012-11-16 20:30:12 +0000
3+++ lib/lp/blueprints/model/specification.py 2012-11-22 09:56:03 +0000
4@@ -50,8 +50,10 @@
5 InformationType,
6 PUBLIC_INFORMATION_TYPES,
7 )
8+from lp.app.enums import PRIVATE_INFORMATION_TYPES
9 from lp.app.errors import UserCannotUnsubscribePerson
10 from lp.app.interfaces.informationtype import IInformationType
11+from lp.app.interfaces.services import IService
12 from lp.app.model.launchpad import InformationTypeMixin
13 from lp.blueprints.adapters import SpecificationDelta
14 from lp.blueprints.enums import (
15@@ -86,6 +88,10 @@
16 from lp.bugs.model.buglinktarget import BugLinkTargetMixin
17 from lp.registry.enums import SpecificationSharingPolicy
18 from lp.registry.errors import CannotChangeInformationType
19+from lp.registry.interfaces.accesspolicy import (
20+ IAccessArtifactGrantSource,
21+ IAccessArtifactSource,
22+ )
23 from lp.registry.interfaces.distribution import IDistribution
24 from lp.registry.interfaces.distroseries import IDistroSeries
25 from lp.registry.interfaces.person import validate_public_person
26@@ -728,6 +734,15 @@
27 property_cache.subscriptions.append(sub)
28 property_cache.subscriptions.sort(
29 key=lambda sub: person_sort_key(sub.person))
30+ if self.information_type in PRIVATE_INFORMATION_TYPES:
31+ # Grant the subscriber access if they can't see the
32+ # specification.
33+ service = getUtility(IService, 'sharing')
34+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
35+ person, specifications=[self], ignore_permissions=True)
36+ if not shared_specs:
37+ service.ensureAccessGrants(
38+ [person], subscribed_by, specifications=[self])
39 notify(ObjectCreatedEvent(sub, user=subscribed_by))
40 return sub
41
42@@ -746,6 +761,10 @@
43 person.displayname))
44 get_property_cache(self).subscriptions.remove(sub)
45 SpecificationSubscription.delete(sub.id)
46+ artifacts_to_delete = getUtility(
47+ IAccessArtifactSource).find([self])
48+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
49+ artifacts_to_delete, [person])
50 return
51
52 def isSubscribed(self, person):
53@@ -875,6 +894,16 @@
54 raise CannotChangeInformationType("Forbidden by project policy.")
55 self.information_type = information_type
56 reconcile_access_for_artifact(self, information_type, [self.target])
57+ if information_type in PRIVATE_INFORMATION_TYPES and self.subscribers:
58+ # Grant the subscribers access if they do not have a
59+ # policy grant.
60+ service = getUtility(IService, 'sharing')
61+ blind_subscribers = service.getPeopleWithoutAccess(
62+ self, self.subscribers)
63+ if len(blind_subscribers):
64+ service.ensureAccessGrants(
65+ blind_subscribers, who, specifications=[self],
66+ ignore_permissions=True)
67 return True
68
69 @cachedproperty
70
71=== modified file 'lib/lp/blueprints/model/specificationsubscription.py'
72--- lib/lp/blueprints/model/specificationsubscription.py 2011-12-30 06:14:56 +0000
73+++ lib/lp/blueprints/model/specificationsubscription.py 2012-11-22 09:56:03 +0000
74@@ -11,11 +11,17 @@
75 BoolCol,
76 ForeignKey,
77 )
78+from zope.component import getUtility
79 from zope.interface import implements
80
81 from lp.blueprints.interfaces.specificationsubscription import (
82 ISpecificationSubscription,
83 )
84+from lp.registry.interfaces.accesspolicy import (
85+ IAccessArtifactGrantSource,
86+ IAccessArtifactSource,
87+ )
88+from lp.registry.interfaces.role import IPersonRoles
89 from lp.registry.interfaces.person import validate_person
90 from lp.services.database.sqlbase import SQLBase
91
92@@ -37,6 +43,27 @@
93 """See `ISpecificationSubscription`."""
94 if user is None:
95 return False
96- if self.person.is_team:
97- return user.inTeam(self.person)
98- return user == self.person
99+ if not IPersonRoles.providedBy(user):
100+ user = IPersonRoles(user)
101+ if (
102+ user.inTeam(self.specification.owner) or
103+ user.inTeam(self.person) or
104+ user.in_admin):
105+ return True
106+ # XXX Abel Deuring 2012-11-21, bug=1081677
107+ # People who subscribed users should be able to unsubscribe
108+ # them again, similar to branch subscriptions. This is
109+ # essential if somebody was erroneuosly subscribed to a
110+ # proprietary or embargoed specification. Unfortunately,
111+ # SpecificationSubscription does not record who subscribed
112+ # somebody else, but if the specification is private, we can
113+ # check who issued the artifact grant.
114+ artifacts = getUtility(IAccessArtifactSource).find(
115+ [self.specification])
116+ wanted = [(artifact, self.person) for artifact in artifacts]
117+ if len(wanted) == 0:
118+ return False
119+ for grant in getUtility(IAccessArtifactGrantSource).find(wanted):
120+ if user.inTeam(grant.grantor):
121+ return True
122+ return False
123
124=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
125--- lib/lp/blueprints/model/tests/test_specification.py 2012-09-17 15:19:10 +0000
126+++ lib/lp/blueprints/model/tests/test_specification.py 2012-11-22 09:56:03 +0000
127@@ -5,6 +5,8 @@
128
129 __metaclass__ = type
130
131+from zope.component import getUtility
132+
133 from lazr.lifecycle.event import ObjectModifiedEvent
134 from lazr.lifecycle.snapshot import Snapshot
135 from testtools.matchers import (
136@@ -19,13 +21,17 @@
137 from zope.security.proxy import removeSecurityProxy
138
139 from lp.app.enums import InformationType
140+from lp.app.interfaces.services import IService
141 from lp.app.validators import LaunchpadValidationError
142 from lp.blueprints.interfaces.specification import ISpecification
143 from lp.blueprints.interfaces.specificationworkitem import (
144 SpecificationWorkItemStatus,
145 )
146 from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
147-from lp.registry.enums import SpecificationSharingPolicy
148+from lp.registry.enums import (
149+ SharingPermission,
150+ SpecificationSharingPolicy,
151+ )
152 from lp.registry.errors import CannotChangeInformationType
153 from lp.registry.model.milestone import Milestone
154 from lp.services.mail import stub
155@@ -615,9 +621,9 @@
156
157 def test_transitionToInformationType(self):
158 """Ensure transitionToInformationType works."""
159+ public_private = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
160 product = self.factory.makeProduct(
161- specification_sharing_policy=
162- SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)
163+ specification_sharing_policy=public_private)
164 spec = self.factory.makeSpecification(product=product)
165 self.assertEqual(InformationType.PUBLIC, spec.information_type)
166 removeSecurityProxy(spec.target)._ensurePolicies(
167@@ -643,3 +649,45 @@
168 with person_logged_in(spec.owner):
169 with ExpectedException(CannotChangeInformationType, '.*'):
170 spec.transitionToInformationType(None, spec.owner)
171+
172+ def test_transitionToInformationType_adds_grants_for_subscribers(self):
173+ # Subscribers are automatically granted access when the
174+ # new information type requires a grant.
175+ owner = self.factory.makePerson()
176+ public_private = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
177+ product = self.factory.makeProduct(
178+ owner=owner,
179+ specification_sharing_policy=public_private)
180+ spec = self.factory.makeSpecification(product=product)
181+ subscriber_with_policy_grant = self.factory.makePerson()
182+ subscriber_without_policy_grant = self.factory.makePerson()
183+ service = getUtility(IService, 'sharing')
184+ with person_logged_in(owner):
185+ service.sharePillarInformation(
186+ product, subscriber_with_policy_grant, owner,
187+ permissions={
188+ InformationType.PROPRIETARY: SharingPermission.ALL,
189+ })
190+ spec.subscribe(subscriber_with_policy_grant, owner)
191+ spec.subscribe(subscriber_without_policy_grant, owner)
192+
193+ # The specification is public, hence subscribers do not need
194+ # and do not have access grants.
195+ self.assertEqual(
196+ [], service.getSharedSpecifications(
197+ product, subscriber_without_policy_grant, owner))
198+ self.assertEqual(
199+ [], service.getSharedSpecifications(
200+ product, subscriber_with_policy_grant, owner))
201+
202+ spec.transitionToInformationType(
203+ InformationType.PROPRIETARY, owner)
204+ # transitionToInformationType() added an artifact grant for
205+ # subscriber_without_policy_grant.
206+ self.assertEqual(
207+ [spec], service.getSharedSpecifications(
208+ product, subscriber_without_policy_grant, owner))
209+ # No access grant was created for subscriber_with_policy_grant.
210+ self.assertEqual(
211+ [], service.getSharedSpecifications(
212+ product, subscriber_with_policy_grant, owner))
213
214=== modified file 'lib/lp/blueprints/model/tests/test_subscription.py'
215--- lib/lp/blueprints/model/tests/test_subscription.py 2012-01-01 02:58:52 +0000
216+++ lib/lp/blueprints/model/tests/test_subscription.py 2012-11-22 09:56:03 +0000
217@@ -3,7 +3,15 @@
218
219 __metaclass__ = type
220
221+from zope.component import getUtility
222+
223+from lp.app.enums import InformationType
224 from lp.app.errors import UserCannotUnsubscribePerson
225+from lp.app.interfaces.services import IService
226+from lp.registry.enums import (
227+ SharingPermission,
228+ SpecificationSharingPolicy,
229+ )
230 from lp.testing import (
231 person_logged_in,
232 TestCaseWithFactory,
233@@ -21,11 +29,35 @@
234
235 layer = DatabaseFunctionalLayer
236
237- def _make_subscription(self):
238- spec = self.factory.makeSpecification()
239+ def _make_subscription(self, proprietary_subscription=False):
240 subscriber = self.factory.makePerson()
241 subscribed_by = self.factory.makePerson()
242- subscription = spec.subscribe(subscriber, subscribed_by)
243+ policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
244+ product_owner = self.factory.makePerson()
245+ product = self.factory.makeProduct(
246+ specification_sharing_policy=policy, owner=product_owner)
247+ if proprietary_subscription:
248+ info_type = InformationType.PROPRIETARY
249+ with person_logged_in(product_owner):
250+ permissions = {
251+ InformationType.PROPRIETARY: SharingPermission.ALL,
252+ }
253+ getUtility(IService, 'sharing').sharePillarInformation(
254+ product, subscribed_by, product_owner, permissions)
255+ else:
256+ info_type = InformationType.PUBLIC
257+ if proprietary_subscription:
258+ # If the spec is proprietary, subscribed_by must have the
259+ # permission launchpad.Edit on the spec in order to
260+ # subscribe someone. This permission requires to have a
261+ # special role for the specificaiton, like the assignee.
262+ assignee = subscribed_by
263+ else:
264+ assignee = None
265+ spec = self.factory.makeSpecification(
266+ product=product, information_type=info_type, assignee=assignee)
267+ with person_logged_in(subscribed_by):
268+ subscription = spec.subscribe(subscriber, subscribed_by)
269 return spec, subscriber, subscribed_by, subscription
270
271 def test_can_unsubscribe_self(self):
272@@ -35,13 +67,23 @@
273 subscribed_by, subscription) = self._make_subscription()
274 self.assertTrue(subscription.canBeUnsubscribedByUser(subscriber))
275
276- def test_subscriber_cannot_unsubscribe_user(self):
277- # The one who subscribed the subscriber doesn't have permission to
278- # unsubscribe him.
279+ # XXX Abel Deuring 2012-11-21, bug=1081677
280+ # The two tests below show a weird inconsisteny: Sometimes
281+ # subscribed_by can unsubscribe, sometimes not.
282+ def test_subscriber_cannot_unsubscribe_user_from_public_spec(self):
283+ # For public specifications, the one who subscribed the
284+ # subscriber doesn't have permission to unsubscribe him.
285 (spec, subscriber,
286 subscribed_by, subscription) = self._make_subscription()
287 self.assertFalse(subscription.canBeUnsubscribedByUser(subscribed_by))
288
289+ def test_subscriber_can_unsubscribe_user_from_private_spec(self):
290+ # For private specifications, the one who subscribed the
291+ # subscriber has permission to unsubscribe him.
292+ (spec, subscriber,
293+ subscribed_by, subscription) = self._make_subscription(True)
294+ self.assertTrue(subscription.canBeUnsubscribedByUser(subscribed_by))
295+
296 def test_anonymous_cannot_unsubscribe(self):
297 # The anonymous user (represented by None) can't unsubscribe anyone.
298 (spec, subscriber,
299@@ -84,3 +126,16 @@
300 person = self.factory.makePerson()
301 self.assertRaises(
302 UserCannotUnsubscribePerson, spec.unsubscribe, subscriber, person)
303+
304+ def test_spec_owner_can_unsubscribe(self):
305+ # The owner of a specification can unsubscribe any subscriber.
306+ (spec, subscriber,
307+ subscribed_by, subscription) = self._make_subscription()
308+ self.assertTrue(subscription.canBeUnsubscribedByUser(spec.owner))
309+
310+ def test_admin_can_unsubscribe(self):
311+ # LP admins can unsubscribe any subscriber.
312+ (spec, subscriber,
313+ subscribed_by, subscription) = self._make_subscription()
314+ admin = self.factory.makeAdministrator()
315+ self.assertTrue(subscription.canBeUnsubscribedByUser(admin))
316
317=== modified file 'lib/lp/blueprints/tests/test_specification.py'
318--- lib/lp/blueprints/tests/test_specification.py 2012-11-15 22:02:33 +0000
319+++ lib/lp/blueprints/tests/test_specification.py 2012-11-22 09:56:03 +0000
320@@ -442,6 +442,62 @@
321 self.assertContentEqual(
322 [public_spec, proprietary_spec_1], specs_for_other_user)
323
324+ def test_subscribe_to_proprietary_spec(self):
325+ # If users are subscribed to a proprietary specification,
326+ # they are automatically granted access to the specification.
327+ owner = self.factory.makePerson()
328+ spec_sharing_policy = SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC
329+ product = self.factory.makeProduct(
330+ owner=owner, specification_sharing_policy=spec_sharing_policy)
331+ with person_logged_in(owner):
332+ user = self.factory.makePerson()
333+ spec = self.factory.makeSpecification(
334+ product=product,
335+ information_type=InformationType.PROPRIETARY)
336+ spec.subscribe(user, subscribed_by=owner)
337+ service = getUtility(IService, 'sharing')
338+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
339+ user, specifications=[spec])
340+ self.assertEqual([spec], shared_specs)
341+ # The spec is also returned by getSharedSpecifications(),
342+ # which lists only specifications for which the use has
343+ # an artifact grant.
344+ self.assertEqual(
345+ [spec], service.getSharedSpecifications(product, user, owner))
346+ # Users which have a policy grants for the spec's target
347+ # do not get an additional artifact grant...
348+ user_2 = self.factory.makePerson()
349+ permissions = {
350+ InformationType.PROPRIETARY: SharingPermission.ALL,
351+ }
352+ service.sharePillarInformation(
353+ product, user_2, owner, permissions)
354+ spec.subscribe(user_2, subscribed_by=owner)
355+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
356+ user_2, specifications=[spec])
357+ self.assertEqual([spec], shared_specs)
358+ self.assertEqual(
359+ [], service.getSharedSpecifications(product, user_2, owner))
360+
361+ def test_unsubscribe_from_proprietary_spec(self):
362+ # If users are unsubscribed from a proprietary specification,
363+ # a related artifact grant is deleted too.
364+ owner = self.factory.makePerson()
365+ spec_sharing_policy = SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC
366+ product = self.factory.makeProduct(
367+ owner=owner, specification_sharing_policy=spec_sharing_policy)
368+ with person_logged_in(owner):
369+ user = self.factory.makePerson()
370+ spec = self.factory.makeSpecification(
371+ product=product,
372+ information_type=InformationType.PROPRIETARY)
373+ spec.subscribe(user, subscribed_by=owner)
374+ spec.unsubscribe(user, unsubscribed_by=owner)
375+ service = getUtility(IService, 'sharing')
376+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
377+ user, specifications=[spec])
378+ self.assertEqual([], shared_specs)
379+
380
381 class TestSpecificationSet(TestCaseWithFactory):
382
383
384=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
385--- lib/lp/blueprints/tests/test_webservice.py 2012-10-09 10:28:02 +0000
386+++ lib/lp/blueprints/tests/test_webservice.py 2012-11-22 09:56:03 +0000
387@@ -352,7 +352,7 @@
388
389 result = webservice.named_get(
390 subscription['self_link'], 'canBeUnsubscribedByUser').jsonBody()
391- self.assertFalse(result)
392+ self.assertTrue(result)
393
394
395 class TestSpecificationBugLinks(SpecificationWebserviceTestCase):