Merge lp:~wallyworld/launchpad/sharing-details-delete2-966641 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15067
Proposed branch: lp:~wallyworld/launchpad/sharing-details-delete2-966641
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/sharing-details-delete-966641
Diff against target: 323 lines (+198/-12)
8 files modified
lib/lp/registry/browser/pillar.py (+1/-1)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+1/-1)
lib/lp/registry/interfaces/accesspolicy.py (+15/-5)
lib/lp/registry/interfaces/sharingservice.py (+20/-0)
lib/lp/registry/model/accesspolicy.py (+9/-5)
lib/lp/registry/services/sharingservice.py (+21/-0)
lib/lp/registry/services/tests/test_sharingservice.py (+97/-0)
lib/lp/registry/tests/test_accesspolicy.py (+34/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/sharing-details-delete2-966641
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+100723@code.launchpad.net

Commit message

Add sharing service api for revoking access to bugs and branches

Description of the change

== Implementation ==

Add new API to sharing service:

    def revokeAccessGrants(pillar, sharee, branches=None, bugs=None):
        """Remove a sharee's access to the specified artifacts.

This is used by the delete button on the sharing details page to revoke a user's access to a bug or branch.

The findByArtifact and revokeByArtifact methods on IAccessArtifactGrantSource had to be extended to allow an option list of grantees to be passed in, to only find/revoke access for the specified people.

I also had to change the sharing details view - it was putting bugtask urls into the view data model but instead needed to use bug urls. Our disclosure data model deals with bugs, not bugtasks.

== Tests ==

Add new revokeAccessGrants tests to test_sharingservice
Add new tests for the findByArtifact and revokeByArtifact methods to test_accesspolicy

== Lint ==

Linting changed files:
  lib/lp/registry/interfaces/accesspolicy.py
  lib/lp/registry/interfaces/sharingservice.py
  lib/lp/registry/model/accesspolicy.py
  lib/lp/registry/services/sharingservice.py
  lib/lp/registry/services/tests/test_sharingservice.py
  lib/lp/registry/tests/test_accesspolicy.py

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

I do not understand line 23. user-data and security bugs can be shared by projects. I may only maintain the second or third project, so the following code
    importance = bug.default_bugtask.importance.title.lower()
returns the importance the oldest bugtask found. Since my project is not the oldest task, it is not the default -- that importance is wrong.

Also the links are made to the bug, which I think chooses the first/oldest bugtask as well -- the context is not my project so the bugtask index page renders the links in the side portlets for the other project, not mine. I think test_pillar_sharing could have a test with for a second task that demonstrates that this code is providing data for the wrong task.

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

> I do not understand line 23. user-data and security bugs can be shared by
> projects. I may only maintain the second or third project, so the following
> code
> importance = bug.default_bugtask.importance.title.lower()
> returns the importance the oldest bugtask found. Since my project is not the
> oldest task, it is not the default -- that importance is wrong.
>
> Also the links are made to the bug, which I think chooses the first/oldest
> bugtask as well -- the context is not my project so the bugtask index page
> renders the links in the side portlets for the other project, not mine. I
> think test_pillar_sharing could have a test with for a second task that
> demonstrates that this code is providing data for the wrong task.

The core issue was that the view needs to display bugtask information but when it makes the API call to revoke access when the user hits "Delete", it needs to send the bug url not the bugtask url. I incorrectly implemented this change. The view data model has been corrected to include the bugtask attributes for the importance and rendered url and the self_link attribute is set to that of the bug which is what is sent through to the API call. Essentially, the code was reverted to how it used to be; only the self_link attribute was added.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py 2012-04-04 23:34:24 +0000
+++ lib/lp/registry/browser/pillar.py 2012-04-04 23:34:24 +0000
@@ -431,7 +431,7 @@
431 task.target == self.pillar]431 task.target == self.pillar]
432 if bugtask is not None:432 if bugtask is not None:
433 web_link = canonical_url(bugtask, path_only_if_possible=True)433 web_link = canonical_url(bugtask, path_only_if_possible=True)
434 self_link = absoluteURL(bugtask, request)434 self_link = absoluteURL(bug, request)
435 importance = bugtask.importance.title.lower()435 importance = bugtask.importance.title.lower()
436 else:436 else:
437 # This shouldn't ever happen, but if it does there's no reason437 # This shouldn't ever happen, but if it does there's no reason
438438
=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-04 23:34:24 +0000
+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-04 23:34:24 +0000
@@ -149,7 +149,7 @@
149 'bug_importance': bugtask.importance.title.lower(),149 'bug_importance': bugtask.importance.title.lower(),
150 'web_link': canonical_url(150 'web_link': canonical_url(
151 bugtask, path_only_if_possible=True),151 bugtask, path_only_if_possible=True),
152 'self_link': absoluteURL(bugtask, request),152 'self_link': absoluteURL(self.bug, request),
153 }, cache.objects.get('bugs')[0])153 }, cache.objects.get('bugs')[0])
154 if self.pillar_type == 'product':154 if self.pillar_type == 'product':
155 self.assertEqual({155 self.assertEqual({
156156
=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
--- lib/lp/registry/interfaces/accesspolicy.py 2012-03-31 18:22:38 +0000
+++ lib/lp/registry/interfaces/accesspolicy.py 2012-04-04 23:34:24 +0000
@@ -124,11 +124,21 @@
124 pairs.124 pairs.
125 """125 """
126126
127 def findByArtifact(artifacts):127 def findByArtifact(artifacts, grantees=None):
128 """Return all `IAccessArtifactGrant` objects for the artifacts."""128 """Return `IAccessArtifactGrant` objects for the artifacts.
129129
130 def revokeByArtifact(artifacts):130 :param artifacts: the artifacts for which to find any grants.
131 """Delete all `IAccessArtifactGrant` objects for the artifacts."""131 :param grantees: find grants for the specified grantees only,
132 else find all grants.
133 """
134
135 def revokeByArtifact(artifacts, grantees=None):
136 """Delete `IAccessArtifactGrant` objects for the artifacts.
137
138 :param artifacts: the artifacts to which revoke access.
139 :param grantees: revoke access for the specified grantees only,
140 else delete all grants.
141 """
132142
133143
134class IAccessPolicyArtifactSource(Interface):144class IAccessPolicyArtifactSource(Interface):
135145
=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py 2012-03-31 11:32:15 +0000
+++ lib/lp/registry/interfaces/sharingservice.py 2012-04-04 23:34:24 +0000
@@ -28,6 +28,8 @@
2828
29from lp import _29from lp import _
30from lp.app.interfaces.services import IService30from lp.app.interfaces.services import IService
31from lp.bugs.interfaces.bug import IBug
32from lp.code.interfaces.branch import IBranch
31from lp.registry.enums import (33from lp.registry.enums import (
32 InformationType,34 InformationType,
33 SharingPermission,35 SharingPermission,
@@ -119,3 +121,21 @@
119 :param information_types: if None, remove all access, otherwise just121 :param information_types: if None, remove all access, otherwise just
120 remove the specified access_policies122 remove the specified access_policies
121 """123 """
124
125 @export_write_operation()
126 @operation_parameters(
127 pillar=Reference(IPillar, title=_('Pillar'), required=True),
128 sharee=Reference(IPerson, title=_('Sharee'), required=True),
129 bugs=List(
130 Reference(schema=IBug), title=_('Bugs'), required=False),
131 branches=List(
132 Reference(schema=IBranch), title=_('Branches'), required=False))
133 @operation_for_version('devel')
134 def revokeAccessGrants(pillar, sharee, branches=None, bugs=None):
135 """Remove a sharee's access to the specified artifacts.
136
137 :param pillar: the pillar from which to remove access
138 :param sharee: the person or team for whom to revoke access
139 :param bugs: the bugs for which to revoke access
140 :param branches: the branches for which to revoke access
141 """
122142
=== modified file 'lib/lp/registry/model/accesspolicy.py'
--- lib/lp/registry/model/accesspolicy.py 2012-03-31 18:22:38 +0000
+++ lib/lp/registry/model/accesspolicy.py 2012-04-04 23:34:24 +0000
@@ -283,15 +283,19 @@
283 for (artifact, grantee) in grants)))283 for (artifact, grantee) in grants)))
284284
285 @classmethod285 @classmethod
286 def findByArtifact(cls, artifacts):286 def findByArtifact(cls, artifacts, grantees=None):
287 """See `IAccessArtifactGrantSource`."""287 """See `IAccessArtifactGrantSource`."""
288 ids = [artifact.id for artifact in artifacts]288 artifact_ids = [artifact.id for artifact in artifacts]
289 return IStore(cls).find(cls, cls.abstract_artifact_id.is_in(ids))289 constraints = [cls.abstract_artifact_id.is_in(artifact_ids)]
290 if grantees:
291 grantee_ids = [grantee.id for grantee in grantees]
292 constraints.append(cls.grantee_id.is_in(grantee_ids))
293 return IStore(cls).find(cls, *constraints)
290294
291 @classmethod295 @classmethod
292 def revokeByArtifact(cls, artifacts):296 def revokeByArtifact(cls, artifacts, grantees=None):
293 """See `IAccessPolicyGrantSource`."""297 """See `IAccessPolicyGrantSource`."""
294 cls.findByArtifact(artifacts).remove()298 cls.findByArtifact(artifacts, grantees).remove()
295299
296300
297class AccessPolicyGrant(StormBase):301class AccessPolicyGrant(StormBase):
298302
=== modified file 'lib/lp/registry/services/sharingservice.py'
--- lib/lp/registry/services/sharingservice.py 2012-04-02 03:12:58 +0000
+++ lib/lp/registry/services/sharingservice.py 2012-04-04 23:34:24 +0000
@@ -21,6 +21,7 @@
21 )21 )
22from lp.registry.interfaces.accesspolicy import (22from lp.registry.interfaces.accesspolicy import (
23 IAccessArtifactGrantSource,23 IAccessArtifactGrantSource,
24 IAccessArtifactSource,
24 IAccessPolicyGrantFlatSource,25 IAccessPolicyGrantFlatSource,
25 IAccessPolicyGrantSource,26 IAccessPolicyGrantSource,
26 IAccessPolicySource,27 IAccessPolicySource,
@@ -249,3 +250,23 @@
249 accessartifact_grant_source = getUtility(250 accessartifact_grant_source = getUtility(
250 IAccessArtifactGrantSource)251 IAccessArtifactGrantSource)
251 accessartifact_grant_source.revokeByArtifact(to_delete)252 accessartifact_grant_source.revokeByArtifact(to_delete)
253
254 @available_with_permission('launchpad.Edit', 'pillar')
255 def revokeAccessGrants(self, pillar, sharee, branches=None, bugs=None):
256 """See `ISharingService`."""
257
258 if not self.write_enabled:
259 raise Unauthorized("This feature is not yet enabled.")
260
261 artifacts = []
262 if branches:
263 artifacts.extend(branches)
264 if bugs:
265 artifacts.extend(bugs)
266 # Find the access artifacts associated with the bugs and branches.
267 accessartifact_source = getUtility(IAccessArtifactSource)
268 artifacts_to_delete = accessartifact_source.find(artifacts)
269 # Revoke access to bugs/branches for the specified sharee.
270 accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
271 accessartifact_grant_source.revokeByArtifact(
272 artifacts_to_delete, [sharee])
252273
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2012-04-04 04:31:31 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-04-04 23:34:24 +0000
@@ -18,6 +18,8 @@
18 SharingPermission,18 SharingPermission,
19 )19 )
20from lp.registry.interfaces.accesspolicy import (20from lp.registry.interfaces.accesspolicy import (
21 IAccessArtifactGrantSource,
22 IAccessPolicyGrantFlatSource,
21 IAccessPolicyGrantSource,23 IAccessPolicyGrantSource,
22 IAccessPolicySource,24 IAccessPolicySource,
23 )25 )
@@ -586,6 +588,101 @@
586 Unauthorized, self.service.deletePillarSharee,588 Unauthorized, self.service.deletePillarSharee,
587 product, [InformationType.USERDATA])589 product, [InformationType.USERDATA])
588590
591 def _assert_revokeAccessGrants(self, pillar, bugs, branches):
592 artifacts = []
593 if bugs:
594 artifacts.extend(bugs)
595 if branches:
596 artifacts.extend(branches)
597 policy = self.factory.makeAccessPolicy(pillar=pillar)
598 # Grant access to a grantee and another person.
599 grantee = self.factory.makePerson()
600 someone = self.factory.makePerson()
601 access_artifacts = []
602 for artifact in artifacts:
603 access_artifact = self.factory.makeAccessArtifact(
604 concrete=artifact)
605 access_artifacts.append(access_artifact)
606 self.factory.makeAccessPolicyArtifact(
607 artifact=access_artifact, policy=policy)
608 for person in [grantee, someone]:
609 self.factory.makeAccessArtifactGrant(
610 artifact=access_artifact, grantee=person,
611 grantor=pillar.owner)
612
613 # Check that grantee has expected access grants.
614 accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
615 grants = accessartifact_grant_source.findByArtifact(
616 access_artifacts, [grantee])
617 apgfs = getUtility(IAccessPolicyGrantFlatSource)
618 self.assertEqual(1, grants.count())
619
620 with FeatureFixture(WRITE_FLAG):
621 self.service.revokeAccessGrants(
622 pillar, grantee, bugs=bugs, branches=branches)
623
624 # The grantee now has no access to anything.
625 permission_info = apgfs.findGranteePermissionsByPolicy(
626 [policy], [grantee])
627 self.assertEqual(0, permission_info.count())
628
629 # Someone else still has access to the bugs and branches.
630 grants = accessartifact_grant_source.findByArtifact(
631 access_artifacts, [someone])
632 self.assertEqual(1, grants.count())
633
634 def test_revokeAccessGrantsBugs(self):
635 # Users with launchpad.Edit can delete all access for a sharee.
636 owner = self.factory.makePerson()
637 distro = self.factory.makeDistribution(owner=owner)
638 login_person(owner)
639 bug = self.factory.makeBug(
640 distribution=distro, owner=owner, private=True)
641 self._assert_revokeAccessGrants(distro, [bug], None)
642
643 def test_revokeAccessGrantsBranches(self):
644 # Users with launchpad.Edit can delete all access for a sharee.
645 owner = self.factory.makePerson()
646 product = self.factory.makeProduct(owner=owner)
647 login_person(owner)
648 branch = self.factory.makeBranch(
649 product=product, owner=owner, private=True)
650 self._assert_revokeAccessGrants(product, None, [branch])
651
652 def _assert_revokeAccessGrantsUnauthorized(self):
653 # revokeAccessGrants raises an Unauthorized exception if the user
654 # is not permitted to do so.
655 product = self.factory.makeProduct()
656 bug = self.factory.makeBug(product=product, private=True)
657 sharee = self.factory.makePerson()
658 with FeatureFixture(WRITE_FLAG):
659 self.assertRaises(
660 Unauthorized, self.service.revokeAccessGrants,
661 product, sharee, bugs=[bug])
662
663 def test_revokeAccessGrantsAnonymous(self):
664 # Anonymous users are not allowed.
665 with FeatureFixture(WRITE_FLAG):
666 login(ANONYMOUS)
667 self._assert_revokeAccessGrantsUnauthorized()
668
669 def test_revokeAccessGrantsAnyone(self):
670 # Unauthorized users are not allowed.
671 with FeatureFixture(WRITE_FLAG):
672 login_person(self.factory.makePerson())
673 self._assert_revokeAccessGrantsUnauthorized()
674
675 def test_revokeAccessGrants_without_flag(self):
676 # The feature flag needs to be enabled.
677 owner = self.factory.makePerson()
678 product = self.factory.makeProduct(owner=owner)
679 bug = self.factory.makeBug(product=product, private=True)
680 sharee = self.factory.makePerson()
681 login_person(owner)
682 self.assertRaises(
683 Unauthorized, self.service.revokeAccessGrants,
684 product, sharee, bugs=[bug])
685
589686
590class ApiTestMixin:687class ApiTestMixin:
591 """Common tests for launchpadlib and webservice."""688 """Common tests for launchpadlib and webservice."""
592689
=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
--- lib/lp/registry/tests/test_accesspolicy.py 2012-03-31 18:22:38 +0000
+++ lib/lp/registry/tests/test_accesspolicy.py 2012-04-04 23:34:24 +0000
@@ -290,6 +290,21 @@
290 grants,290 grants,
291 getUtility(IAccessArtifactGrantSource).findByArtifact([artifact]))291 getUtility(IAccessArtifactGrantSource).findByArtifact([artifact]))
292292
293 def test_findByArtifact_specified_grantees(self):
294 # findByArtifact() finds only the relevant grants for the specified
295 # grantees.
296 artifact = self.factory.makeAccessArtifact()
297 grantees = [self.factory.makePerson() for i in range(3)]
298 grants = [
299 self.factory.makeAccessArtifactGrant(
300 artifact=artifact, grantee=grantee)
301 for grantee in grantees]
302 self.factory.makeAccessArtifactGrant()
303 self.assertContentEqual(
304 grants[:2],
305 getUtility(IAccessArtifactGrantSource).findByArtifact(
306 [artifact], grantees=grantees[:2]))
307
293 def test_revokeByArtifact(self):308 def test_revokeByArtifact(self):
294 # revokeByArtifact() removes the relevant grants.309 # revokeByArtifact() removes the relevant grants.
295 artifact = self.factory.makeAccessArtifact()310 artifact = self.factory.makeAccessArtifact()
@@ -300,6 +315,25 @@
300 self.assertRaises(LostObjectError, getattr, grant, 'grantor')315 self.assertRaises(LostObjectError, getattr, grant, 'grantor')
301 self.assertIsNot(None, other_grant.grantor)316 self.assertIsNot(None, other_grant.grantor)
302317
318 def test_revokeByArtifact_specified_grantees(self):
319 # revokeByArtifact() removes the relevant grants for the specified
320 # grantees.
321 artifact = self.factory.makeAccessArtifact()
322 grantee = self.factory.makePerson()
323 someone_else = self.factory.makePerson()
324 grant = self.factory.makeAccessArtifactGrant(
325 artifact=artifact, grantee=grantee)
326 someone_else_grant = self.factory.makeAccessArtifactGrant(
327 artifact=artifact, grantee=someone_else)
328 other_grant = self.factory.makeAccessArtifactGrant()
329 aags = getUtility(IAccessArtifactGrantSource)
330 aags.revokeByArtifact([artifact], [grantee])
331 IStore(grant).invalidate()
332 self.assertRaises(LostObjectError, getattr, grant, 'grantor')
333 self.assertEqual(
334 someone_else_grant, aags.findByArtifact([artifact])[0])
335 self.assertIsNot(None, other_grant.grantor)
336
303337
304class TestAccessPolicyArtifact(TestCaseWithFactory):338class TestAccessPolicyArtifact(TestCaseWithFactory):
305 layer = DatabaseFunctionalLayer339 layer = DatabaseFunctionalLayer