Merge lp:~adeuring/launchpad/use-access-grants-for-specifications-auth-check into lp:launchpad

Proposed by Abel Deuring on 2012-09-11
Status: Merged
Approved by: Abel Deuring on 2012-09-11
Approved revision: no longer in the source branch.
Merged at revision: 15945
Proposed branch: lp:~adeuring/launchpad/use-access-grants-for-specifications-auth-check
Merge into: lp:launchpad
Diff against target: 336 lines (+155/-36)
5 files modified
lib/lp/blueprints/browser/tests/test_specification.py (+11/-2)
lib/lp/blueprints/model/specification.py (+58/-9)
lib/lp/blueprints/tests/test_specification.py (+81/-23)
lib/lp/registry/model/accesspolicy.py (+1/-0)
lib/lp/testing/factory.py (+4/-2)
To merge this branch: bzr merge lp:~adeuring/launchpad/use-access-grants-for-specifications-auth-check
Reviewer Review Type Date Requested Status
Richard Harding (community) 2012-09-11 Approve on 2012-09-11
Review via email: mp+123774@code.launchpad.net

Commit Message

Use access grant tables for permission checks of ISpeicifcation.

Description of the Change

This branch changes the metod SPeicifcation.userCanView() so that it
uses access grants to check if a user may view a given specification.

For non-public specifications, this requires an access grant on either
the specification's target or on the specification itself.

The related tests required some changes. Most importantly, calling
specification.transitionToInformationType() raises currently
Unauthorized because the owner of a specification does not get at present
automatically a view grant if the specification is made non-public.

This should be fixed of course, but can be done in another branch --
it does not matter here, if transitionToInformationType() is called by
a product owner or a specification owner.

The test test_special_user_read_access() is now obsolete because
userCanView() does no longer check a user's role in the project.

test:
./bin/test -vvt lp.blueprints.tests.test_specification

no lint

To post a comment you must log in.
Richard Harding (rharding) :
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/browser/tests/test_specification.py'
2--- lib/lp/blueprints/browser/tests/test_specification.py 2012-09-07 08:07:48 +0000
3+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-09-12 12:21:23 +0000
4@@ -22,6 +22,7 @@
5 from zope.security.proxy import removeSecurityProxy
6
7 from lp.app.browser.tales import format_link
8+from lp.app.interfaces.services import IService
9 from lp.blueprints.browser import specification
10 from lp.blueprints.enums import SpecificationImplementationStatus
11 from lp.blueprints.interfaces.specification import (
12@@ -202,9 +203,17 @@
13
14 def test_has_privacy_banner(self):
15 owner = self.factory.makePerson()
16+ target = self.factory.makeProduct()
17+ removeSecurityProxy(target)._ensurePolicies(
18+ [InformationType.PROPRIETARY])
19 spec = self.factory.makeSpecification(
20- information_type=InformationType.PROPRIETARY, owner=owner)
21- browser = self.getViewBrowser(spec, user=owner)
22+ information_type=InformationType.PROPRIETARY, owner=owner,
23+ product=target)
24+ with person_logged_in(target.owner):
25+ getUtility(IService, 'sharing').ensureAccessGrants(
26+ [owner], target.owner, specifications=[spec])
27+ with person_logged_in(owner):
28+ browser = self.getViewBrowser(spec, user=owner)
29 privacy_banner = soupmatchers.Tag('privacy-banner', True,
30 attrs={'class': 'banner-text'})
31 self.assertThat(browser.contents,
32
33=== modified file 'lib/lp/blueprints/model/specification.py'
34--- lib/lp/blueprints/model/specification.py 2012-09-10 08:30:42 +0000
35+++ lib/lp/blueprints/model/specification.py 2012-09-12 12:21:23 +0000
36@@ -25,6 +25,13 @@
37 SQLRelatedJoin,
38 StringCol,
39 )
40+from storm.expr import (
41+ And,
42+ In,
43+ Join,
44+ Or,
45+ Select,
46+ )
47 from storm.locals import (
48 Desc,
49 SQL,
50@@ -78,6 +85,7 @@
51 from lp.registry.interfaces.person import validate_public_person
52 from lp.registry.interfaces.product import IProduct
53 from lp.registry.interfaces.productseries import IProductSeries
54+from lp.registry.model.teammembership import TeamParticipation
55 from lp.services.database.constants import (
56 DEFAULT,
57 UTC_NOW,
58@@ -838,21 +846,62 @@
59 def private(self):
60 return self.information_type in PRIVATE_INFORMATION_TYPES
61
62+ @cachedproperty
63+ def _known_viewers(self):
64+ """A set of known persons able to view the specifcation."""
65+ return set()
66+
67 def userCanView(self, user):
68 """See `ISpecification`."""
69+ # Avoid circular imports.
70+ from lp.registry.model.accesspolicy import (
71+ AccessArtifact,
72+ AccessPolicy,
73+ AccessPolicyGrantFlat,
74+ )
75 if self.information_type in PUBLIC_INFORMATION_TYPES:
76 return True
77 if user is None:
78 return False
79- # Temporary: we should access the grant tables instead of
80- # checking if a given user has special roles.
81- # The following is basically copied from
82- # EditSpecificationByRelatedPeople.checkAuthenticated()
83- return (user.in_admin or
84- user.isOwner(self.target) or
85- user.isOneOfDrivers(self.target) or
86- user.isOneOf(
87- self, ['owner', 'drafter', 'assignee', 'approver']))
88+ if user.id in self._known_viewers:
89+ return True
90+
91+ # Check if access has been granted to the user for either
92+ # the pillar of this specification or the specification
93+ # itself.
94+ #
95+ # A DB constraint ensures that either Specification.product or
96+ # Specification.distribution is not null.
97+ if self.product is not None:
98+ pillar_clause = AccessPolicy.product == self.productID
99+ else:
100+ pillar_clause = AccessPolicy.distribution == self.distributionID
101+ tables = (
102+ AccessPolicyGrantFlat,
103+ Join(
104+ AccessPolicy,
105+ AccessPolicyGrantFlat.policy_id == AccessPolicy.id),
106+ Join(
107+ TeamParticipation,
108+ AccessPolicyGrantFlat.grantee == TeamParticipation.teamID)
109+ )
110+ grants = Store.of(self).using(*tables).find(
111+ AccessPolicyGrantFlat,
112+ pillar_clause,
113+ Or(
114+ And(
115+ AccessPolicyGrantFlat.abstract_artifact == None,
116+ AccessPolicy.type == self.information_type),
117+ In(
118+ AccessPolicyGrantFlat.abstract_artifact_id,
119+ Select(
120+ AccessArtifact.id,
121+ AccessArtifact.specification_id == self.id))),
122+ TeamParticipation.personID == user.id)
123+ if grants.is_empty():
124+ return False
125+ self._known_viewers.add(user.id)
126+ return True
127
128
129 class HasSpecificationsMixin:
130
131=== modified file 'lib/lp/blueprints/tests/test_specification.py'
132--- lib/lp/blueprints/tests/test_specification.py 2012-09-10 10:20:39 +0000
133+++ lib/lp/blueprints/tests/test_specification.py 2012-09-12 12:21:23 +0000
134@@ -18,6 +18,7 @@
135 from zope.security.proxy import removeSecurityProxy
136
137 from lp.app.interfaces.security import IAuthorization
138+from lp.app.interfaces.services import IService
139 from lp.blueprints.enums import (
140 NewSpecificationDefinitionStatus,
141 SpecificationDefinitionStatus,
142@@ -26,8 +27,10 @@
143 from lp.blueprints.errors import TargetAlreadyHasSpecification
144 from lp.blueprints.interfaces.specification import ISpecificationSet
145 from lp.registry.enums import (
146+ InformationType,
147 PRIVATE_INFORMATION_TYPES,
148 PUBLIC_INFORMATION_TYPES,
149+ SharingPermission,
150 )
151 from lp.security import (
152 AdminSpecification,
153@@ -35,6 +38,7 @@
154 EditWhiteboardSpecification,
155 ViewSpecification,
156 )
157+from lp.services.propertycache import get_property_cache
158 from lp.services.webapp.authorization import check_permission
159 from lp.services.webapp.interaction import ANONYMOUS
160 from lp.testing import (
161@@ -256,7 +260,7 @@
162 PRIVATE_INFORMATION_TYPES)
163 all_types = specification.getAllowedInformationTypes(ANONYMOUS)
164 for information_type in all_types:
165- with person_logged_in(specification.owner):
166+ with person_logged_in(specification.target.owner):
167 specification.transitionToInformationType(
168 information_type, specification.owner)
169 error_expected = information_type not in PUBLIC_INFORMATION_TYPES
170@@ -270,7 +274,7 @@
171 PRIVATE_INFORMATION_TYPES)
172 all_types = specification.getAllowedInformationTypes(ANONYMOUS)
173 for information_type in all_types:
174- with person_logged_in(specification.owner):
175+ with person_logged_in(specification.target.owner):
176 specification.transitionToInformationType(
177 information_type, specification.owner)
178 self.write_access_to_ISpecificationView(
179@@ -289,7 +293,7 @@
180 user = self.factory.makePerson()
181 all_types = specification.getAllowedInformationTypes(user)
182 for information_type in all_types:
183- with person_logged_in(specification.owner):
184+ with person_logged_in(specification.target.owner):
185 specification.transitionToInformationType(
186 information_type, specification.owner)
187 error_expected = information_type not in PUBLIC_INFORMATION_TYPES
188@@ -306,7 +310,7 @@
189 user = self.factory.makePerson()
190 all_types = specification.getAllowedInformationTypes(user)
191 for information_type in all_types:
192- with person_logged_in(specification.owner):
193+ with person_logged_in(specification.target.owner):
194 specification.transitionToInformationType(
195 information_type, specification.owner)
196 error_expected = information_type not in PUBLIC_INFORMATION_TYPES
197@@ -317,20 +321,74 @@
198 user, specification, error_expected=True,
199 attribute='name', value='foo')
200
201- def test_special_user_read_access(self):
202- # Users with special privileges can aceess the attributes
203- # of public and private specifcations.
204- specification = self.factory.makeSpecification()
205- removeSecurityProxy(specification.target)._ensurePolicies(
206- PRIVATE_INFORMATION_TYPES)
207- all_types = specification.getAllowedInformationTypes(
208- specification.owner)
209- for information_type in all_types:
210- with person_logged_in(specification.owner):
211- specification.transitionToInformationType(
212- information_type, specification.owner)
213- self.read_access_to_ISpecificationView(
214- specification.owner, specification, error_expected=False)
215+ def test_user_with_grant_for_target_read_access(self):
216+ # Users with a grant for the specification's target
217+ # have access to a specification if the information_type
218+ # of the specification matches the type if the grant.
219+ specification = self.factory.makeSpecification()
220+ removeSecurityProxy(specification.target)._ensurePolicies(
221+ PRIVATE_INFORMATION_TYPES)
222+ user = self.factory.makePerson()
223+ permissions = {
224+ InformationType.PROPRIETARY: SharingPermission.ALL,
225+ }
226+ with person_logged_in(specification.target.owner):
227+ getUtility(IService, 'sharing').sharePillarInformation(
228+ specification.target, user, specification.target.owner,
229+ permissions)
230+ all_types = specification.getAllowedInformationTypes(user)
231+ for information_type in all_types:
232+ with person_logged_in(specification.target.owner):
233+ specification.transitionToInformationType(
234+ information_type, specification.owner)
235+ error_expected = (
236+ information_type not in PUBLIC_INFORMATION_TYPES and
237+ information_type not in permissions)
238+ self.read_access_to_ISpecificationView(
239+ user, specification, error_expected)
240+ del get_property_cache(specification)._known_viewers
241+
242+ def test_user_with_grant_for_specification_read_access(self):
243+ # Users with a grant for the specification have access to this
244+ # specification.
245+ specification = self.factory.makeSpecification()
246+ removeSecurityProxy(specification.target)._ensurePolicies(
247+ PRIVATE_INFORMATION_TYPES)
248+ user = self.factory.makePerson()
249+ with person_logged_in(specification.target.owner):
250+ getUtility(IService, 'sharing').ensureAccessGrants(
251+ [user], specification.target.owner,
252+ specifications=[specification], ignore_permissions=True)
253+ all_types = specification.getAllowedInformationTypes(user)
254+ for information_type in all_types:
255+ with person_logged_in(specification.target.owner):
256+ specification.transitionToInformationType(
257+ information_type, specification.owner)
258+ self.read_access_to_ISpecificationView(
259+ user, specification, error_expected=False)
260+
261+ def test_user_with_grant_for_specification_write_access(self):
262+ # Users with a grant for the specification can change the whiteboard
263+ # but no other attributes.
264+ specification = self.factory.makeSpecification()
265+ removeSecurityProxy(specification.target)._ensurePolicies(
266+ PRIVATE_INFORMATION_TYPES)
267+ user = self.factory.makePerson()
268+ with person_logged_in(specification.target.owner):
269+ getUtility(IService, 'sharing').ensureAccessGrants(
270+ [user], specification.target.owner,
271+ specifications=[specification], ignore_permissions=True)
272+ all_types = specification.getAllowedInformationTypes(user)
273+ for information_type in all_types:
274+ with person_logged_in(specification.target.owner):
275+ specification.transitionToInformationType(
276+ information_type, specification.owner)
277+ self.write_access_to_ISpecificationView(
278+ user, specification, error_expected=False,
279+ attribute='whiteboard', value='foo')
280+ self.write_access_to_ISpecificationView(
281+ user, specification, error_expected=True,
282+ attribute='name', value='foo')
283
284 def test_special_user_write_access(self):
285 # Users with special privileges can change the attributes
286@@ -341,15 +399,15 @@
287 all_types = specification.getAllowedInformationTypes(
288 specification.owner)
289 for information_type in all_types:
290- with person_logged_in(specification.owner):
291+ with person_logged_in(specification.target.owner):
292 specification.transitionToInformationType(
293 information_type, specification.owner)
294 self.write_access_to_ISpecificationView(
295- specification.owner, specification, error_expected=False,
296- attribute='whiteboard', value='foo')
297+ specification.target.owner, specification,
298+ error_expected=False, attribute='whiteboard', value='foo')
299 self.write_access_to_ISpecificationView(
300- specification.owner, specification, error_expected=False,
301- attribute='name', value='foo')
302+ specification.target.owner, specification,
303+ error_expected=False, attribute='name', value='foo')
304
305
306 class TestSpecificationSet(TestCaseWithFactory):
307
308=== modified file 'lib/lp/registry/model/accesspolicy.py'
309--- lib/lp/registry/model/accesspolicy.py 2012-09-04 10:35:22 +0000
310+++ lib/lp/registry/model/accesspolicy.py 2012-09-12 12:21:23 +0000
311@@ -10,6 +10,7 @@
312 'AccessPolicy',
313 'AccessPolicyArtifact',
314 'AccessPolicyGrant',
315+ 'AccessPolicyGrantFlat',
316 'reconcile_access_for_artifact',
317 ]
318
319
320=== modified file 'lib/lp/testing/factory.py'
321--- lib/lp/testing/factory.py 2012-08-31 14:35:33 +0000
322+++ lib/lp/testing/factory.py 2012-09-12 12:21:23 +0000
323@@ -2107,9 +2107,11 @@
324 approver=approver,
325 product=product,
326 distribution=distribution,
327- priority=priority,
328- information_type=information_type)
329+ priority=priority)
330 naked_spec = removeSecurityProxy(spec)
331+ if information_type is not None:
332+ naked_spec.transitionToInformationType(
333+ information_type, spec.target.owner)
334 if status.name not in status_names:
335 # Set the closed status after the status has a sane initial state.
336 naked_spec.definition_status = status