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
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2012-11-16 20:30:12 +0000
+++ lib/lp/blueprints/model/specification.py 2012-11-22 09:56:03 +0000
@@ -50,8 +50,10 @@
50 InformationType,50 InformationType,
51 PUBLIC_INFORMATION_TYPES,51 PUBLIC_INFORMATION_TYPES,
52 )52 )
53from lp.app.enums import PRIVATE_INFORMATION_TYPES
53from lp.app.errors import UserCannotUnsubscribePerson54from lp.app.errors import UserCannotUnsubscribePerson
54from lp.app.interfaces.informationtype import IInformationType55from lp.app.interfaces.informationtype import IInformationType
56from lp.app.interfaces.services import IService
55from lp.app.model.launchpad import InformationTypeMixin57from lp.app.model.launchpad import InformationTypeMixin
56from lp.blueprints.adapters import SpecificationDelta58from lp.blueprints.adapters import SpecificationDelta
57from lp.blueprints.enums import (59from lp.blueprints.enums import (
@@ -86,6 +88,10 @@
86from lp.bugs.model.buglinktarget import BugLinkTargetMixin88from lp.bugs.model.buglinktarget import BugLinkTargetMixin
87from lp.registry.enums import SpecificationSharingPolicy89from lp.registry.enums import SpecificationSharingPolicy
88from lp.registry.errors import CannotChangeInformationType90from lp.registry.errors import CannotChangeInformationType
91from lp.registry.interfaces.accesspolicy import (
92 IAccessArtifactGrantSource,
93 IAccessArtifactSource,
94 )
89from lp.registry.interfaces.distribution import IDistribution95from lp.registry.interfaces.distribution import IDistribution
90from lp.registry.interfaces.distroseries import IDistroSeries96from lp.registry.interfaces.distroseries import IDistroSeries
91from lp.registry.interfaces.person import validate_public_person97from lp.registry.interfaces.person import validate_public_person
@@ -728,6 +734,15 @@
728 property_cache.subscriptions.append(sub)734 property_cache.subscriptions.append(sub)
729 property_cache.subscriptions.sort(735 property_cache.subscriptions.sort(
730 key=lambda sub: person_sort_key(sub.person))736 key=lambda sub: person_sort_key(sub.person))
737 if self.information_type in PRIVATE_INFORMATION_TYPES:
738 # Grant the subscriber access if they can't see the
739 # specification.
740 service = getUtility(IService, 'sharing')
741 ignored, ignored, shared_specs = service.getVisibleArtifacts(
742 person, specifications=[self], ignore_permissions=True)
743 if not shared_specs:
744 service.ensureAccessGrants(
745 [person], subscribed_by, specifications=[self])
731 notify(ObjectCreatedEvent(sub, user=subscribed_by))746 notify(ObjectCreatedEvent(sub, user=subscribed_by))
732 return sub747 return sub
733748
@@ -746,6 +761,10 @@
746 person.displayname))761 person.displayname))
747 get_property_cache(self).subscriptions.remove(sub)762 get_property_cache(self).subscriptions.remove(sub)
748 SpecificationSubscription.delete(sub.id)763 SpecificationSubscription.delete(sub.id)
764 artifacts_to_delete = getUtility(
765 IAccessArtifactSource).find([self])
766 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
767 artifacts_to_delete, [person])
749 return768 return
750769
751 def isSubscribed(self, person):770 def isSubscribed(self, person):
@@ -875,6 +894,16 @@
875 raise CannotChangeInformationType("Forbidden by project policy.")894 raise CannotChangeInformationType("Forbidden by project policy.")
876 self.information_type = information_type895 self.information_type = information_type
877 reconcile_access_for_artifact(self, information_type, [self.target])896 reconcile_access_for_artifact(self, information_type, [self.target])
897 if information_type in PRIVATE_INFORMATION_TYPES and self.subscribers:
898 # Grant the subscribers access if they do not have a
899 # policy grant.
900 service = getUtility(IService, 'sharing')
901 blind_subscribers = service.getPeopleWithoutAccess(
902 self, self.subscribers)
903 if len(blind_subscribers):
904 service.ensureAccessGrants(
905 blind_subscribers, who, specifications=[self],
906 ignore_permissions=True)
878 return True907 return True
879908
880 @cachedproperty909 @cachedproperty
881910
=== modified file 'lib/lp/blueprints/model/specificationsubscription.py'
--- lib/lp/blueprints/model/specificationsubscription.py 2011-12-30 06:14:56 +0000
+++ lib/lp/blueprints/model/specificationsubscription.py 2012-11-22 09:56:03 +0000
@@ -11,11 +11,17 @@
11 BoolCol,11 BoolCol,
12 ForeignKey,12 ForeignKey,
13 )13 )
14from zope.component import getUtility
14from zope.interface import implements15from zope.interface import implements
1516
16from lp.blueprints.interfaces.specificationsubscription import (17from lp.blueprints.interfaces.specificationsubscription import (
17 ISpecificationSubscription,18 ISpecificationSubscription,
18 )19 )
20from lp.registry.interfaces.accesspolicy import (
21 IAccessArtifactGrantSource,
22 IAccessArtifactSource,
23 )
24from lp.registry.interfaces.role import IPersonRoles
19from lp.registry.interfaces.person import validate_person25from lp.registry.interfaces.person import validate_person
20from lp.services.database.sqlbase import SQLBase26from lp.services.database.sqlbase import SQLBase
2127
@@ -37,6 +43,27 @@
37 """See `ISpecificationSubscription`."""43 """See `ISpecificationSubscription`."""
38 if user is None:44 if user is None:
39 return False45 return False
40 if self.person.is_team:46 if not IPersonRoles.providedBy(user):
41 return user.inTeam(self.person)47 user = IPersonRoles(user)
42 return user == self.person48 if (
49 user.inTeam(self.specification.owner) or
50 user.inTeam(self.person) or
51 user.in_admin):
52 return True
53 # XXX Abel Deuring 2012-11-21, bug=1081677
54 # People who subscribed users should be able to unsubscribe
55 # them again, similar to branch subscriptions. This is
56 # essential if somebody was erroneuosly subscribed to a
57 # proprietary or embargoed specification. Unfortunately,
58 # SpecificationSubscription does not record who subscribed
59 # somebody else, but if the specification is private, we can
60 # check who issued the artifact grant.
61 artifacts = getUtility(IAccessArtifactSource).find(
62 [self.specification])
63 wanted = [(artifact, self.person) for artifact in artifacts]
64 if len(wanted) == 0:
65 return False
66 for grant in getUtility(IAccessArtifactGrantSource).find(wanted):
67 if user.inTeam(grant.grantor):
68 return True
69 return False
4370
=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
--- lib/lp/blueprints/model/tests/test_specification.py 2012-09-17 15:19:10 +0000
+++ lib/lp/blueprints/model/tests/test_specification.py 2012-11-22 09:56:03 +0000
@@ -5,6 +5,8 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from zope.component import getUtility
9
8from lazr.lifecycle.event import ObjectModifiedEvent10from lazr.lifecycle.event import ObjectModifiedEvent
9from lazr.lifecycle.snapshot import Snapshot11from lazr.lifecycle.snapshot import Snapshot
10from testtools.matchers import (12from testtools.matchers import (
@@ -19,13 +21,17 @@
19from zope.security.proxy import removeSecurityProxy21from zope.security.proxy import removeSecurityProxy
2022
21from lp.app.enums import InformationType23from lp.app.enums import InformationType
24from lp.app.interfaces.services import IService
22from lp.app.validators import LaunchpadValidationError25from lp.app.validators import LaunchpadValidationError
23from lp.blueprints.interfaces.specification import ISpecification26from lp.blueprints.interfaces.specification import ISpecification
24from lp.blueprints.interfaces.specificationworkitem import (27from lp.blueprints.interfaces.specificationworkitem import (
25 SpecificationWorkItemStatus,28 SpecificationWorkItemStatus,
26 )29 )
27from lp.blueprints.model.specificationworkitem import SpecificationWorkItem30from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
28from lp.registry.enums import SpecificationSharingPolicy31from lp.registry.enums import (
32 SharingPermission,
33 SpecificationSharingPolicy,
34 )
29from lp.registry.errors import CannotChangeInformationType35from lp.registry.errors import CannotChangeInformationType
30from lp.registry.model.milestone import Milestone36from lp.registry.model.milestone import Milestone
31from lp.services.mail import stub37from lp.services.mail import stub
@@ -615,9 +621,9 @@
615621
616 def test_transitionToInformationType(self):622 def test_transitionToInformationType(self):
617 """Ensure transitionToInformationType works."""623 """Ensure transitionToInformationType works."""
624 public_private = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
618 product = self.factory.makeProduct(625 product = self.factory.makeProduct(
619 specification_sharing_policy=626 specification_sharing_policy=public_private)
620 SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)
621 spec = self.factory.makeSpecification(product=product)627 spec = self.factory.makeSpecification(product=product)
622 self.assertEqual(InformationType.PUBLIC, spec.information_type)628 self.assertEqual(InformationType.PUBLIC, spec.information_type)
623 removeSecurityProxy(spec.target)._ensurePolicies(629 removeSecurityProxy(spec.target)._ensurePolicies(
@@ -643,3 +649,45 @@
643 with person_logged_in(spec.owner):649 with person_logged_in(spec.owner):
644 with ExpectedException(CannotChangeInformationType, '.*'):650 with ExpectedException(CannotChangeInformationType, '.*'):
645 spec.transitionToInformationType(None, spec.owner)651 spec.transitionToInformationType(None, spec.owner)
652
653 def test_transitionToInformationType_adds_grants_for_subscribers(self):
654 # Subscribers are automatically granted access when the
655 # new information type requires a grant.
656 owner = self.factory.makePerson()
657 public_private = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
658 product = self.factory.makeProduct(
659 owner=owner,
660 specification_sharing_policy=public_private)
661 spec = self.factory.makeSpecification(product=product)
662 subscriber_with_policy_grant = self.factory.makePerson()
663 subscriber_without_policy_grant = self.factory.makePerson()
664 service = getUtility(IService, 'sharing')
665 with person_logged_in(owner):
666 service.sharePillarInformation(
667 product, subscriber_with_policy_grant, owner,
668 permissions={
669 InformationType.PROPRIETARY: SharingPermission.ALL,
670 })
671 spec.subscribe(subscriber_with_policy_grant, owner)
672 spec.subscribe(subscriber_without_policy_grant, owner)
673
674 # The specification is public, hence subscribers do not need
675 # and do not have access grants.
676 self.assertEqual(
677 [], service.getSharedSpecifications(
678 product, subscriber_without_policy_grant, owner))
679 self.assertEqual(
680 [], service.getSharedSpecifications(
681 product, subscriber_with_policy_grant, owner))
682
683 spec.transitionToInformationType(
684 InformationType.PROPRIETARY, owner)
685 # transitionToInformationType() added an artifact grant for
686 # subscriber_without_policy_grant.
687 self.assertEqual(
688 [spec], service.getSharedSpecifications(
689 product, subscriber_without_policy_grant, owner))
690 # No access grant was created for subscriber_with_policy_grant.
691 self.assertEqual(
692 [], service.getSharedSpecifications(
693 product, subscriber_with_policy_grant, owner))
646694
=== modified file 'lib/lp/blueprints/model/tests/test_subscription.py'
--- lib/lp/blueprints/model/tests/test_subscription.py 2012-01-01 02:58:52 +0000
+++ lib/lp/blueprints/model/tests/test_subscription.py 2012-11-22 09:56:03 +0000
@@ -3,7 +3,15 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from zope.component import getUtility
7
8from lp.app.enums import InformationType
6from lp.app.errors import UserCannotUnsubscribePerson9from lp.app.errors import UserCannotUnsubscribePerson
10from lp.app.interfaces.services import IService
11from lp.registry.enums import (
12 SharingPermission,
13 SpecificationSharingPolicy,
14 )
7from lp.testing import (15from lp.testing import (
8 person_logged_in,16 person_logged_in,
9 TestCaseWithFactory,17 TestCaseWithFactory,
@@ -21,11 +29,35 @@
2129
22 layer = DatabaseFunctionalLayer30 layer = DatabaseFunctionalLayer
2331
24 def _make_subscription(self):32 def _make_subscription(self, proprietary_subscription=False):
25 spec = self.factory.makeSpecification()
26 subscriber = self.factory.makePerson()33 subscriber = self.factory.makePerson()
27 subscribed_by = self.factory.makePerson()34 subscribed_by = self.factory.makePerson()
28 subscription = spec.subscribe(subscriber, subscribed_by)35 policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
36 product_owner = self.factory.makePerson()
37 product = self.factory.makeProduct(
38 specification_sharing_policy=policy, owner=product_owner)
39 if proprietary_subscription:
40 info_type = InformationType.PROPRIETARY
41 with person_logged_in(product_owner):
42 permissions = {
43 InformationType.PROPRIETARY: SharingPermission.ALL,
44 }
45 getUtility(IService, 'sharing').sharePillarInformation(
46 product, subscribed_by, product_owner, permissions)
47 else:
48 info_type = InformationType.PUBLIC
49 if proprietary_subscription:
50 # If the spec is proprietary, subscribed_by must have the
51 # permission launchpad.Edit on the spec in order to
52 # subscribe someone. This permission requires to have a
53 # special role for the specificaiton, like the assignee.
54 assignee = subscribed_by
55 else:
56 assignee = None
57 spec = self.factory.makeSpecification(
58 product=product, information_type=info_type, assignee=assignee)
59 with person_logged_in(subscribed_by):
60 subscription = spec.subscribe(subscriber, subscribed_by)
29 return spec, subscriber, subscribed_by, subscription61 return spec, subscriber, subscribed_by, subscription
3062
31 def test_can_unsubscribe_self(self):63 def test_can_unsubscribe_self(self):
@@ -35,13 +67,23 @@
35 subscribed_by, subscription) = self._make_subscription()67 subscribed_by, subscription) = self._make_subscription()
36 self.assertTrue(subscription.canBeUnsubscribedByUser(subscriber))68 self.assertTrue(subscription.canBeUnsubscribedByUser(subscriber))
3769
38 def test_subscriber_cannot_unsubscribe_user(self):70 # XXX Abel Deuring 2012-11-21, bug=1081677
39 # The one who subscribed the subscriber doesn't have permission to71 # The two tests below show a weird inconsisteny: Sometimes
40 # unsubscribe him.72 # subscribed_by can unsubscribe, sometimes not.
73 def test_subscriber_cannot_unsubscribe_user_from_public_spec(self):
74 # For public specifications, the one who subscribed the
75 # subscriber doesn't have permission to unsubscribe him.
41 (spec, subscriber,76 (spec, subscriber,
42 subscribed_by, subscription) = self._make_subscription()77 subscribed_by, subscription) = self._make_subscription()
43 self.assertFalse(subscription.canBeUnsubscribedByUser(subscribed_by))78 self.assertFalse(subscription.canBeUnsubscribedByUser(subscribed_by))
4479
80 def test_subscriber_can_unsubscribe_user_from_private_spec(self):
81 # For private specifications, the one who subscribed the
82 # subscriber has permission to unsubscribe him.
83 (spec, subscriber,
84 subscribed_by, subscription) = self._make_subscription(True)
85 self.assertTrue(subscription.canBeUnsubscribedByUser(subscribed_by))
86
45 def test_anonymous_cannot_unsubscribe(self):87 def test_anonymous_cannot_unsubscribe(self):
46 # The anonymous user (represented by None) can't unsubscribe anyone.88 # The anonymous user (represented by None) can't unsubscribe anyone.
47 (spec, subscriber,89 (spec, subscriber,
@@ -84,3 +126,16 @@
84 person = self.factory.makePerson()126 person = self.factory.makePerson()
85 self.assertRaises(127 self.assertRaises(
86 UserCannotUnsubscribePerson, spec.unsubscribe, subscriber, person)128 UserCannotUnsubscribePerson, spec.unsubscribe, subscriber, person)
129
130 def test_spec_owner_can_unsubscribe(self):
131 # The owner of a specification can unsubscribe any subscriber.
132 (spec, subscriber,
133 subscribed_by, subscription) = self._make_subscription()
134 self.assertTrue(subscription.canBeUnsubscribedByUser(spec.owner))
135
136 def test_admin_can_unsubscribe(self):
137 # LP admins can unsubscribe any subscriber.
138 (spec, subscriber,
139 subscribed_by, subscription) = self._make_subscription()
140 admin = self.factory.makeAdministrator()
141 self.assertTrue(subscription.canBeUnsubscribedByUser(admin))
87142
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py 2012-11-15 22:02:33 +0000
+++ lib/lp/blueprints/tests/test_specification.py 2012-11-22 09:56:03 +0000
@@ -442,6 +442,62 @@
442 self.assertContentEqual(442 self.assertContentEqual(
443 [public_spec, proprietary_spec_1], specs_for_other_user)443 [public_spec, proprietary_spec_1], specs_for_other_user)
444444
445 def test_subscribe_to_proprietary_spec(self):
446 # If users are subscribed to a proprietary specification,
447 # they are automatically granted access to the specification.
448 owner = self.factory.makePerson()
449 spec_sharing_policy = SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC
450 product = self.factory.makeProduct(
451 owner=owner, specification_sharing_policy=spec_sharing_policy)
452 with person_logged_in(owner):
453 user = self.factory.makePerson()
454 spec = self.factory.makeSpecification(
455 product=product,
456 information_type=InformationType.PROPRIETARY)
457 spec.subscribe(user, subscribed_by=owner)
458 service = getUtility(IService, 'sharing')
459 ignored, ignored, shared_specs = service.getVisibleArtifacts(
460 user, specifications=[spec])
461 self.assertEqual([spec], shared_specs)
462 # The spec is also returned by getSharedSpecifications(),
463 # which lists only specifications for which the use has
464 # an artifact grant.
465 self.assertEqual(
466 [spec], service.getSharedSpecifications(product, user, owner))
467 # Users which have a policy grants for the spec's target
468 # do not get an additional artifact grant...
469 user_2 = self.factory.makePerson()
470 permissions = {
471 InformationType.PROPRIETARY: SharingPermission.ALL,
472 }
473 service.sharePillarInformation(
474 product, user_2, owner, permissions)
475 spec.subscribe(user_2, subscribed_by=owner)
476 ignored, ignored, shared_specs = service.getVisibleArtifacts(
477 user_2, specifications=[spec])
478 self.assertEqual([spec], shared_specs)
479 self.assertEqual(
480 [], service.getSharedSpecifications(product, user_2, owner))
481
482 def test_unsubscribe_from_proprietary_spec(self):
483 # If users are unsubscribed from a proprietary specification,
484 # a related artifact grant is deleted too.
485 owner = self.factory.makePerson()
486 spec_sharing_policy = SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC
487 product = self.factory.makeProduct(
488 owner=owner, specification_sharing_policy=spec_sharing_policy)
489 with person_logged_in(owner):
490 user = self.factory.makePerson()
491 spec = self.factory.makeSpecification(
492 product=product,
493 information_type=InformationType.PROPRIETARY)
494 spec.subscribe(user, subscribed_by=owner)
495 spec.unsubscribe(user, unsubscribed_by=owner)
496 service = getUtility(IService, 'sharing')
497 ignored, ignored, shared_specs = service.getVisibleArtifacts(
498 user, specifications=[spec])
499 self.assertEqual([], shared_specs)
500
445501
446class TestSpecificationSet(TestCaseWithFactory):502class TestSpecificationSet(TestCaseWithFactory):
447503
448504
=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py 2012-10-09 10:28:02 +0000
+++ lib/lp/blueprints/tests/test_webservice.py 2012-11-22 09:56:03 +0000
@@ -352,7 +352,7 @@
352352
353 result = webservice.named_get(353 result = webservice.named_get(
354 subscription['self_link'], 'canBeUnsubscribedByUser').jsonBody()354 subscription['self_link'], 'canBeUnsubscribedByUser').jsonBody()
355 self.assertFalse(result)355 self.assertTrue(result)
356356
357357
358class TestSpecificationBugLinks(SpecificationWebserviceTestCase):358class TestSpecificationBugLinks(SpecificationWebserviceTestCase):