launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14434
[Merge] lp:~adeuring/launchpad/bug-1056881 into lp:launchpad
Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-1056881 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1056881 in Launchpad itself: "Specifications privacy: subscribers can't see private blueprints"
https://bugs.launchpad.net/launchpad/+bug/1056881
For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-1056881/+merge/135374
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)
--
https://code.launchpad.net/~adeuring/launchpad/bug-1056881/+merge/135374
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-1056881 into lp:launchpad.
=== 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-21 11:31:25 +0000
@@ -50,8 +50,10 @@
InformationType,
PUBLIC_INFORMATION_TYPES,
)
+from lp.app.enums import PRIVATE_INFORMATION_TYPES
from lp.app.errors import UserCannotUnsubscribePerson
from lp.app.interfaces.informationtype import IInformationType
+from lp.app.interfaces.services import IService
from lp.app.model.launchpad import InformationTypeMixin
from lp.blueprints.adapters import SpecificationDelta
from lp.blueprints.enums import (
@@ -86,6 +88,10 @@
from lp.bugs.model.buglinktarget import BugLinkTargetMixin
from lp.registry.enums import SpecificationSharingPolicy
from lp.registry.errors import CannotChangeInformationType
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactGrantSource,
+ IAccessArtifactSource,
+ )
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.person import validate_public_person
@@ -728,6 +734,15 @@
property_cache.subscriptions.append(sub)
property_cache.subscriptions.sort(
key=lambda sub: person_sort_key(sub.person))
+ if self.information_type in PRIVATE_INFORMATION_TYPES:
+ # Grant the subscriber access if they can't see the
+ # specification.
+ service = getUtility(IService, 'sharing')
+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
+ person, specifications=[self], ignore_permissions=True)
+ if not shared_specs:
+ service.ensureAccessGrants(
+ [person], subscribed_by, specifications=[self])
notify(ObjectCreatedEvent(sub, user=subscribed_by))
return sub
@@ -746,6 +761,10 @@
person.displayname))
get_property_cache(self).subscriptions.remove(sub)
SpecificationSubscription.delete(sub.id)
+ artifacts_to_delete = getUtility(
+ IAccessArtifactSource).find([self])
+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
+ artifacts_to_delete, [person])
return
def isSubscribed(self, person):
@@ -875,6 +894,16 @@
raise CannotChangeInformationType("Forbidden by project policy.")
self.information_type = information_type
reconcile_access_for_artifact(self, information_type, [self.target])
+ if information_type in PRIVATE_INFORMATION_TYPES and self.subscribers:
+ # Grant the subscribers access if they do not have a
+ # policy grant.
+ service = getUtility(IService, 'sharing')
+ blind_subscribers = service.getPeopleWithoutAccess(
+ self, self.subscribers)
+ if len(blind_subscribers):
+ service.ensureAccessGrants(
+ blind_subscribers, who, specifications=[self],
+ ignore_permissions=True)
return True
@cachedproperty
=== 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-21 11:31:25 +0000
@@ -11,11 +11,17 @@
BoolCol,
ForeignKey,
)
+from zope.component import getUtility
from zope.interface import implements
from lp.blueprints.interfaces.specificationsubscription import (
ISpecificationSubscription,
)
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactGrantSource,
+ IAccessArtifactSource,
+ )
+from lp.registry.interfaces.role import IPersonRoles
from lp.registry.interfaces.person import validate_person
from lp.services.database.sqlbase import SQLBase
@@ -37,6 +43,26 @@
"""See `ISpecificationSubscription`."""
if user is None:
return False
- if self.person.is_team:
- return user.inTeam(self.person)
- return user == self.person
+ if not IPersonRoles.providedBy(user):
+ user = IPersonRoles(user)
+ if (
+ user.inTeam(self.specification.owner) or
+ user.inTeam(self.person) or
+ user.in_admin):
+ return True
+ # People who subscribed users should be able to unsubscribe
+ # them again, similar to branch subscriptions. This is
+ # essential if somebody was erroneuosly subscribed to a
+ # proprietary or embargoed specification. Unfortunately,
+ # SpecificationSubscription does not record who subscribed
+ # somebody else, but if the specification is private, we can
+ # check who issued the artifact grant.
+ artifacts = getUtility(IAccessArtifactSource).find(
+ [self.specification])
+ wanted = [(artifact, self.person) for artifact in artifacts]
+ if len(wanted) == 0:
+ return False
+ for grant in getUtility(IAccessArtifactGrantSource).find(wanted):
+ if user.inTeam(grant.grantor):
+ return True
+ return False
=== 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-21 11:31:25 +0000
@@ -5,6 +5,8 @@
__metaclass__ = type
+from zope.component import getUtility
+
from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.lifecycle.snapshot import Snapshot
from testtools.matchers import (
@@ -19,13 +21,17 @@
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
+from lp.app.interfaces.services import IService
from lp.app.validators import LaunchpadValidationError
from lp.blueprints.interfaces.specification import ISpecification
from lp.blueprints.interfaces.specificationworkitem import (
SpecificationWorkItemStatus,
)
from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
-from lp.registry.enums import SpecificationSharingPolicy
+from lp.registry.enums import (
+ SharingPermission,
+ SpecificationSharingPolicy,
+ )
from lp.registry.errors import CannotChangeInformationType
from lp.registry.model.milestone import Milestone
from lp.services.mail import stub
@@ -643,3 +649,45 @@
with person_logged_in(spec.owner):
with ExpectedException(CannotChangeInformationType, '.*'):
spec.transitionToInformationType(None, spec.owner)
+
+ def test_transitionToInformationType_adds_grants_for_subscribers(self):
+ # Subscribers are automatically granted access when the
+ # new information type requires a grant.
+ owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ owner=owner,
+ specification_sharing_policy=
+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)
+ spec = self.factory.makeSpecification(product=product)
+ subscriber_with_policy_grant = self.factory.makePerson()
+ subscriber_without_policy_grant = self.factory.makePerson()
+ service = getUtility(IService, 'sharing')
+ with person_logged_in(owner):
+ service.sharePillarInformation(
+ product, subscriber_with_policy_grant, owner,
+ permissions={
+ InformationType.PROPRIETARY: SharingPermission.ALL,
+ })
+ spec.subscribe(subscriber_with_policy_grant, owner)
+ spec.subscribe(subscriber_without_policy_grant, owner)
+
+ # The specification is public, hence subscribers do not need
+ # and do not have access grants.
+ self.assertEqual(
+ [], service.getSharedSpecifications(
+ product, subscriber_without_policy_grant, owner))
+ self.assertEqual(
+ [], service.getSharedSpecifications(
+ product, subscriber_with_policy_grant, owner))
+
+ spec.transitionToInformationType(
+ InformationType.PROPRIETARY, owner)
+ # transitionToInformationType() added an artifact grant for
+ # subscriber_without_policy_grant.
+ self.assertEqual(
+ [spec], service.getSharedSpecifications(
+ product, subscriber_without_policy_grant, owner))
+ # No access grant was created for subscriber_with_policy_grant.
+ self.assertEqual(
+ [], service.getSharedSpecifications(
+ product, subscriber_with_policy_grant, owner))
=== 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-21 11:31:25 +0000
@@ -3,7 +3,15 @@
__metaclass__ = type
+from zope.component import getUtility
+
+from lp.app.enums import InformationType
from lp.app.errors import UserCannotUnsubscribePerson
+from lp.app.interfaces.services import IService
+from lp.registry.enums import (
+ SharingPermission,
+ SpecificationSharingPolicy,
+ )
from lp.testing import (
person_logged_in,
TestCaseWithFactory,
@@ -21,11 +29,35 @@
layer = DatabaseFunctionalLayer
- def _make_subscription(self):
- spec = self.factory.makeSpecification()
+ def _make_subscription(self, proprietary_subscription=False):
subscriber = self.factory.makePerson()
subscribed_by = self.factory.makePerson()
- subscription = spec.subscribe(subscriber, subscribed_by)
+ policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
+ product_owner = self.factory.makePerson()
+ product = self.factory.makeProduct(
+ specification_sharing_policy=policy, owner=product_owner)
+ if proprietary_subscription:
+ info_type = InformationType.PROPRIETARY
+ with person_logged_in(product_owner):
+ permissions = {
+ InformationType.PROPRIETARY: SharingPermission.ALL,
+ }
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, subscribed_by, product_owner, permissions)
+ else:
+ info_type = InformationType.PUBLIC
+ if proprietary_subscription:
+ # If the spec is proprietary, subscribed_by must have the
+ # permission launchpad.Edit on the spec in order to
+ # subscribe someone. This permission requires to have a
+ # special role for the specificaiton, like the assignee.
+ assignee = subscribed_by
+ else:
+ assignee = None
+ spec = self.factory.makeSpecification(
+ product=product, information_type=info_type, assignee=assignee)
+ with person_logged_in(subscribed_by):
+ subscription = spec.subscribe(subscriber, subscribed_by)
return spec, subscriber, subscribed_by, subscription
def test_can_unsubscribe_self(self):
@@ -35,13 +67,20 @@
subscribed_by, subscription) = self._make_subscription()
self.assertTrue(subscription.canBeUnsubscribedByUser(subscriber))
- def test_subscriber_cannot_unsubscribe_user(self):
- # The one who subscribed the subscriber doesn't have permission to
- # unsubscribe him.
+ def test_subscriber_cannot_unsubscribe_user_from_public_spec(self):
+ # For public specifications, the one who subscribed the
+ # subscriber doesn't have permission to unsubscribe him.
(spec, subscriber,
subscribed_by, subscription) = self._make_subscription()
self.assertFalse(subscription.canBeUnsubscribedByUser(subscribed_by))
+ def test_subscriber_can_unsubscribe_user_from_private_spec(self):
+ # For private specifications, the one who subscribed the
+ # subscriber has permission to unsubscribe him.
+ (spec, subscriber,
+ subscribed_by, subscription) = self._make_subscription(True)
+ self.assertTrue(subscription.canBeUnsubscribedByUser(subscribed_by))
+
def test_anonymous_cannot_unsubscribe(self):
# The anonymous user (represented by None) can't unsubscribe anyone.
(spec, subscriber,
@@ -84,3 +123,16 @@
person = self.factory.makePerson()
self.assertRaises(
UserCannotUnsubscribePerson, spec.unsubscribe, subscriber, person)
+
+ def test_spec_owner_can_unsubscribe(self):
+ # The owner of a specification can unsubscribe any subscriber.
+ (spec, subscriber,
+ subscribed_by, subscription) = self._make_subscription()
+ self.assertTrue(subscription.canBeUnsubscribedByUser(spec.owner))
+
+ def test_admin_can_unsubscribe(self):
+ # LP admins can unsubscribe any subscriber.
+ (spec, subscriber,
+ subscribed_by, subscription) = self._make_subscription()
+ admin = self.factory.makeAdministrator()
+ self.assertTrue(subscription.canBeUnsubscribedByUser(admin))
=== 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-21 11:31:25 +0000
@@ -442,6 +442,62 @@
self.assertContentEqual(
[public_spec, proprietary_spec_1], specs_for_other_user)
+ def test_subscribe_to_proprietary_spec(self):
+ # If users are subscribed to a proprietary specification,
+ # they are automatically granted access to the specification.
+ owner = self.factory.makePerson()
+ spec_sharing_policy = SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=spec_sharing_policy)
+ with person_logged_in(owner):
+ user = self.factory.makePerson()
+ spec = self.factory.makeSpecification(
+ product=product,
+ information_type=InformationType.PROPRIETARY)
+ spec.subscribe(user, subscribed_by=owner)
+ service = getUtility(IService, 'sharing')
+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
+ user, specifications=[spec])
+ self.assertEqual([spec], shared_specs)
+ # The spec is also returned by getSharedSpecifications(),
+ # which lists only specifications for which the use has
+ # an artifact grant.
+ self.assertEqual(
+ [spec], service.getSharedSpecifications(product, user, owner))
+ # Users which have a policy grants for the spec's target
+ # do not get an additional artifact grant...
+ user_2 = self.factory.makePerson()
+ permissions = {
+ InformationType.PROPRIETARY: SharingPermission.ALL,
+ }
+ service.sharePillarInformation(
+ product, user_2, owner, permissions)
+ spec.subscribe(user_2, subscribed_by=owner)
+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
+ user_2, specifications=[spec])
+ self.assertEqual([spec], shared_specs)
+ self.assertEqual(
+ [], service.getSharedSpecifications(product, user_2, owner))
+
+ def test_unsubscribe_from_proprietary_spec(self):
+ # If users are unsubscribed from a proprietary specification,
+ # a related artifact grant is deleted too.
+ owner = self.factory.makePerson()
+ spec_sharing_policy = SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC
+ product = self.factory.makeProduct(
+ owner=owner, specification_sharing_policy=spec_sharing_policy)
+ with person_logged_in(owner):
+ user = self.factory.makePerson()
+ spec = self.factory.makeSpecification(
+ product=product,
+ information_type=InformationType.PROPRIETARY)
+ spec.subscribe(user, subscribed_by=owner)
+ spec.unsubscribe(user, unsubscribed_by=owner)
+ service = getUtility(IService, 'sharing')
+ ignored, ignored, shared_specs = service.getVisibleArtifacts(
+ user, specifications=[spec])
+ self.assertEqual([], shared_specs)
+
class TestSpecificationSet(TestCaseWithFactory):
Follow ups