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
1=== modified file 'lib/lp/registry/browser/pillar.py'
2--- lib/lp/registry/browser/pillar.py 2012-04-04 23:34:24 +0000
3+++ lib/lp/registry/browser/pillar.py 2012-04-04 23:34:24 +0000
4@@ -431,7 +431,7 @@
5 task.target == self.pillar]
6 if bugtask is not None:
7 web_link = canonical_url(bugtask, path_only_if_possible=True)
8- self_link = absoluteURL(bugtask, request)
9+ self_link = absoluteURL(bug, request)
10 importance = bugtask.importance.title.lower()
11 else:
12 # This shouldn't ever happen, but if it does there's no reason
13
14=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
15--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-04 23:34:24 +0000
16+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-04 23:34:24 +0000
17@@ -149,7 +149,7 @@
18 'bug_importance': bugtask.importance.title.lower(),
19 'web_link': canonical_url(
20 bugtask, path_only_if_possible=True),
21- 'self_link': absoluteURL(bugtask, request),
22+ 'self_link': absoluteURL(self.bug, request),
23 }, cache.objects.get('bugs')[0])
24 if self.pillar_type == 'product':
25 self.assertEqual({
26
27=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
28--- lib/lp/registry/interfaces/accesspolicy.py 2012-03-31 18:22:38 +0000
29+++ lib/lp/registry/interfaces/accesspolicy.py 2012-04-04 23:34:24 +0000
30@@ -124,11 +124,21 @@
31 pairs.
32 """
33
34- def findByArtifact(artifacts):
35- """Return all `IAccessArtifactGrant` objects for the artifacts."""
36-
37- def revokeByArtifact(artifacts):
38- """Delete all `IAccessArtifactGrant` objects for the artifacts."""
39+ def findByArtifact(artifacts, grantees=None):
40+ """Return `IAccessArtifactGrant` objects for the artifacts.
41+
42+ :param artifacts: the artifacts for which to find any grants.
43+ :param grantees: find grants for the specified grantees only,
44+ else find all grants.
45+ """
46+
47+ def revokeByArtifact(artifacts, grantees=None):
48+ """Delete `IAccessArtifactGrant` objects for the artifacts.
49+
50+ :param artifacts: the artifacts to which revoke access.
51+ :param grantees: revoke access for the specified grantees only,
52+ else delete all grants.
53+ """
54
55
56 class IAccessPolicyArtifactSource(Interface):
57
58=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
59--- lib/lp/registry/interfaces/sharingservice.py 2012-03-31 11:32:15 +0000
60+++ lib/lp/registry/interfaces/sharingservice.py 2012-04-04 23:34:24 +0000
61@@ -28,6 +28,8 @@
62
63 from lp import _
64 from lp.app.interfaces.services import IService
65+from lp.bugs.interfaces.bug import IBug
66+from lp.code.interfaces.branch import IBranch
67 from lp.registry.enums import (
68 InformationType,
69 SharingPermission,
70@@ -119,3 +121,21 @@
71 :param information_types: if None, remove all access, otherwise just
72 remove the specified access_policies
73 """
74+
75+ @export_write_operation()
76+ @operation_parameters(
77+ pillar=Reference(IPillar, title=_('Pillar'), required=True),
78+ sharee=Reference(IPerson, title=_('Sharee'), required=True),
79+ bugs=List(
80+ Reference(schema=IBug), title=_('Bugs'), required=False),
81+ branches=List(
82+ Reference(schema=IBranch), title=_('Branches'), required=False))
83+ @operation_for_version('devel')
84+ def revokeAccessGrants(pillar, sharee, branches=None, bugs=None):
85+ """Remove a sharee's access to the specified artifacts.
86+
87+ :param pillar: the pillar from which to remove access
88+ :param sharee: the person or team for whom to revoke access
89+ :param bugs: the bugs for which to revoke access
90+ :param branches: the branches for which to revoke access
91+ """
92
93=== modified file 'lib/lp/registry/model/accesspolicy.py'
94--- lib/lp/registry/model/accesspolicy.py 2012-03-31 18:22:38 +0000
95+++ lib/lp/registry/model/accesspolicy.py 2012-04-04 23:34:24 +0000
96@@ -283,15 +283,19 @@
97 for (artifact, grantee) in grants)))
98
99 @classmethod
100- def findByArtifact(cls, artifacts):
101+ def findByArtifact(cls, artifacts, grantees=None):
102 """See `IAccessArtifactGrantSource`."""
103- ids = [artifact.id for artifact in artifacts]
104- return IStore(cls).find(cls, cls.abstract_artifact_id.is_in(ids))
105+ artifact_ids = [artifact.id for artifact in artifacts]
106+ constraints = [cls.abstract_artifact_id.is_in(artifact_ids)]
107+ if grantees:
108+ grantee_ids = [grantee.id for grantee in grantees]
109+ constraints.append(cls.grantee_id.is_in(grantee_ids))
110+ return IStore(cls).find(cls, *constraints)
111
112 @classmethod
113- def revokeByArtifact(cls, artifacts):
114+ def revokeByArtifact(cls, artifacts, grantees=None):
115 """See `IAccessPolicyGrantSource`."""
116- cls.findByArtifact(artifacts).remove()
117+ cls.findByArtifact(artifacts, grantees).remove()
118
119
120 class AccessPolicyGrant(StormBase):
121
122=== modified file 'lib/lp/registry/services/sharingservice.py'
123--- lib/lp/registry/services/sharingservice.py 2012-04-02 03:12:58 +0000
124+++ lib/lp/registry/services/sharingservice.py 2012-04-04 23:34:24 +0000
125@@ -21,6 +21,7 @@
126 )
127 from lp.registry.interfaces.accesspolicy import (
128 IAccessArtifactGrantSource,
129+ IAccessArtifactSource,
130 IAccessPolicyGrantFlatSource,
131 IAccessPolicyGrantSource,
132 IAccessPolicySource,
133@@ -249,3 +250,23 @@
134 accessartifact_grant_source = getUtility(
135 IAccessArtifactGrantSource)
136 accessartifact_grant_source.revokeByArtifact(to_delete)
137+
138+ @available_with_permission('launchpad.Edit', 'pillar')
139+ def revokeAccessGrants(self, pillar, sharee, branches=None, bugs=None):
140+ """See `ISharingService`."""
141+
142+ if not self.write_enabled:
143+ raise Unauthorized("This feature is not yet enabled.")
144+
145+ artifacts = []
146+ if branches:
147+ artifacts.extend(branches)
148+ if bugs:
149+ artifacts.extend(bugs)
150+ # Find the access artifacts associated with the bugs and branches.
151+ accessartifact_source = getUtility(IAccessArtifactSource)
152+ artifacts_to_delete = accessartifact_source.find(artifacts)
153+ # Revoke access to bugs/branches for the specified sharee.
154+ accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
155+ accessartifact_grant_source.revokeByArtifact(
156+ artifacts_to_delete, [sharee])
157
158=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
159--- lib/lp/registry/services/tests/test_sharingservice.py 2012-04-04 04:31:31 +0000
160+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-04-04 23:34:24 +0000
161@@ -18,6 +18,8 @@
162 SharingPermission,
163 )
164 from lp.registry.interfaces.accesspolicy import (
165+ IAccessArtifactGrantSource,
166+ IAccessPolicyGrantFlatSource,
167 IAccessPolicyGrantSource,
168 IAccessPolicySource,
169 )
170@@ -586,6 +588,101 @@
171 Unauthorized, self.service.deletePillarSharee,
172 product, [InformationType.USERDATA])
173
174+ def _assert_revokeAccessGrants(self, pillar, bugs, branches):
175+ artifacts = []
176+ if bugs:
177+ artifacts.extend(bugs)
178+ if branches:
179+ artifacts.extend(branches)
180+ policy = self.factory.makeAccessPolicy(pillar=pillar)
181+ # Grant access to a grantee and another person.
182+ grantee = self.factory.makePerson()
183+ someone = self.factory.makePerson()
184+ access_artifacts = []
185+ for artifact in artifacts:
186+ access_artifact = self.factory.makeAccessArtifact(
187+ concrete=artifact)
188+ access_artifacts.append(access_artifact)
189+ self.factory.makeAccessPolicyArtifact(
190+ artifact=access_artifact, policy=policy)
191+ for person in [grantee, someone]:
192+ self.factory.makeAccessArtifactGrant(
193+ artifact=access_artifact, grantee=person,
194+ grantor=pillar.owner)
195+
196+ # Check that grantee has expected access grants.
197+ accessartifact_grant_source = getUtility(IAccessArtifactGrantSource)
198+ grants = accessartifact_grant_source.findByArtifact(
199+ access_artifacts, [grantee])
200+ apgfs = getUtility(IAccessPolicyGrantFlatSource)
201+ self.assertEqual(1, grants.count())
202+
203+ with FeatureFixture(WRITE_FLAG):
204+ self.service.revokeAccessGrants(
205+ pillar, grantee, bugs=bugs, branches=branches)
206+
207+ # The grantee now has no access to anything.
208+ permission_info = apgfs.findGranteePermissionsByPolicy(
209+ [policy], [grantee])
210+ self.assertEqual(0, permission_info.count())
211+
212+ # Someone else still has access to the bugs and branches.
213+ grants = accessartifact_grant_source.findByArtifact(
214+ access_artifacts, [someone])
215+ self.assertEqual(1, grants.count())
216+
217+ def test_revokeAccessGrantsBugs(self):
218+ # Users with launchpad.Edit can delete all access for a sharee.
219+ owner = self.factory.makePerson()
220+ distro = self.factory.makeDistribution(owner=owner)
221+ login_person(owner)
222+ bug = self.factory.makeBug(
223+ distribution=distro, owner=owner, private=True)
224+ self._assert_revokeAccessGrants(distro, [bug], None)
225+
226+ def test_revokeAccessGrantsBranches(self):
227+ # Users with launchpad.Edit can delete all access for a sharee.
228+ owner = self.factory.makePerson()
229+ product = self.factory.makeProduct(owner=owner)
230+ login_person(owner)
231+ branch = self.factory.makeBranch(
232+ product=product, owner=owner, private=True)
233+ self._assert_revokeAccessGrants(product, None, [branch])
234+
235+ def _assert_revokeAccessGrantsUnauthorized(self):
236+ # revokeAccessGrants raises an Unauthorized exception if the user
237+ # is not permitted to do so.
238+ product = self.factory.makeProduct()
239+ bug = self.factory.makeBug(product=product, private=True)
240+ sharee = self.factory.makePerson()
241+ with FeatureFixture(WRITE_FLAG):
242+ self.assertRaises(
243+ Unauthorized, self.service.revokeAccessGrants,
244+ product, sharee, bugs=[bug])
245+
246+ def test_revokeAccessGrantsAnonymous(self):
247+ # Anonymous users are not allowed.
248+ with FeatureFixture(WRITE_FLAG):
249+ login(ANONYMOUS)
250+ self._assert_revokeAccessGrantsUnauthorized()
251+
252+ def test_revokeAccessGrantsAnyone(self):
253+ # Unauthorized users are not allowed.
254+ with FeatureFixture(WRITE_FLAG):
255+ login_person(self.factory.makePerson())
256+ self._assert_revokeAccessGrantsUnauthorized()
257+
258+ def test_revokeAccessGrants_without_flag(self):
259+ # The feature flag needs to be enabled.
260+ owner = self.factory.makePerson()
261+ product = self.factory.makeProduct(owner=owner)
262+ bug = self.factory.makeBug(product=product, private=True)
263+ sharee = self.factory.makePerson()
264+ login_person(owner)
265+ self.assertRaises(
266+ Unauthorized, self.service.revokeAccessGrants,
267+ product, sharee, bugs=[bug])
268+
269
270 class ApiTestMixin:
271 """Common tests for launchpadlib and webservice."""
272
273=== modified file 'lib/lp/registry/tests/test_accesspolicy.py'
274--- lib/lp/registry/tests/test_accesspolicy.py 2012-03-31 18:22:38 +0000
275+++ lib/lp/registry/tests/test_accesspolicy.py 2012-04-04 23:34:24 +0000
276@@ -290,6 +290,21 @@
277 grants,
278 getUtility(IAccessArtifactGrantSource).findByArtifact([artifact]))
279
280+ def test_findByArtifact_specified_grantees(self):
281+ # findByArtifact() finds only the relevant grants for the specified
282+ # grantees.
283+ artifact = self.factory.makeAccessArtifact()
284+ grantees = [self.factory.makePerson() for i in range(3)]
285+ grants = [
286+ self.factory.makeAccessArtifactGrant(
287+ artifact=artifact, grantee=grantee)
288+ for grantee in grantees]
289+ self.factory.makeAccessArtifactGrant()
290+ self.assertContentEqual(
291+ grants[:2],
292+ getUtility(IAccessArtifactGrantSource).findByArtifact(
293+ [artifact], grantees=grantees[:2]))
294+
295 def test_revokeByArtifact(self):
296 # revokeByArtifact() removes the relevant grants.
297 artifact = self.factory.makeAccessArtifact()
298@@ -300,6 +315,25 @@
299 self.assertRaises(LostObjectError, getattr, grant, 'grantor')
300 self.assertIsNot(None, other_grant.grantor)
301
302+ def test_revokeByArtifact_specified_grantees(self):
303+ # revokeByArtifact() removes the relevant grants for the specified
304+ # grantees.
305+ artifact = self.factory.makeAccessArtifact()
306+ grantee = self.factory.makePerson()
307+ someone_else = self.factory.makePerson()
308+ grant = self.factory.makeAccessArtifactGrant(
309+ artifact=artifact, grantee=grantee)
310+ someone_else_grant = self.factory.makeAccessArtifactGrant(
311+ artifact=artifact, grantee=someone_else)
312+ other_grant = self.factory.makeAccessArtifactGrant()
313+ aags = getUtility(IAccessArtifactGrantSource)
314+ aags.revokeByArtifact([artifact], [grantee])
315+ IStore(grant).invalidate()
316+ self.assertRaises(LostObjectError, getattr, grant, 'grantor')
317+ self.assertEqual(
318+ someone_else_grant, aags.findByArtifact([artifact])[0])
319+ self.assertIsNot(None, other_grant.grantor)
320+
321
322 class TestAccessPolicyArtifact(TestCaseWithFactory):
323 layer = DatabaseFunctionalLayer