Merge lp:~adeuring/launchpad/correct-permission-check-for-iproduct into lp:launchpad

Proposed by Abel Deuring on 2012-10-02
Status: Merged
Approved by: Abel Deuring on 2012-10-03
Approved revision: no longer in the source branch.
Merged at revision: 16090
Proposed branch: lp:~adeuring/launchpad/correct-permission-check-for-iproduct
Merge into: lp:launchpad
Prerequisite: lp:~adeuring/launchpad/product-sharing-sec-adapter
Diff against target: 1221 lines (+229/-116)
35 files modified
lib/lp/answers/tests/test_question_webservice.py (+5/-3)
lib/lp/app/model/launchpad.py (+1/-1)
lib/lp/blueprints/browser/tests/test_specification.py (+11/-4)
lib/lp/blueprints/browser/tests/test_views.py (+1/-1)
lib/lp/blueprints/tests/test_webservice.py (+6/-4)
lib/lp/bugs/browser/tests/test_bugtask.py (+2/-2)
lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt (+2/-1)
lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt (+2/-1)
lib/lp/bugs/stories/patches-view/patches-view.txt (+2/-0)
lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt (+2/-2)
lib/lp/bugs/stories/webservice/xx-bug.txt (+5/-3)
lib/lp/bugs/tests/test_bugs_webservice.py (+11/-6)
lib/lp/bugs/tests/test_searchtasks_webservice.py (+3/-2)
lib/lp/code/browser/tests/test_branch.py (+1/-1)
lib/lp/code/browser/tests/test_product.py (+4/-2)
lib/lp/code/stories/branches/xx-product-branches.txt (+5/-2)
lib/lp/code/stories/webservice/xx-code-import.txt (+3/-2)
lib/lp/registry/browser/tests/test_milestone.py (+4/-2)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+3/-3)
lib/lp/registry/browser/tests/test_product.py (+1/-1)
lib/lp/registry/configure.zcml (+2/-0)
lib/lp/registry/interfaces/product.py (+2/-14)
lib/lp/registry/model/product.py (+25/-14)
lib/lp/registry/services/tests/test_sharingservice.py (+4/-2)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+1/-1)
lib/lp/registry/tests/test_pillaraffiliation.py (+1/-1)
lib/lp/registry/tests/test_product.py (+85/-26)
lib/lp/registry/tests/test_subscribers.py (+4/-1)
lib/lp/scripts/garbo.py (+2/-2)
lib/lp/scripts/tests/test_garbo.py (+4/-4)
lib/lp/security.py (+7/-3)
lib/lp/testing/factory.py (+8/-2)
lib/lp/translations/browser/tests/test_noindex.py (+6/-1)
lib/lp/translations/stories/importqueue/xx-entry-details.txt (+2/-1)
lib/lp/translations/stories/webservice/xx-potemplate.txt (+2/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/correct-permission-check-for-iproduct
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code 2012-10-02 Approve on 2012-10-03
Review via email: mp+127518@code.launchpad.net

Commit Message

Use access grants to check view permissions for IProduct

Description of the Change

This is a follow-up branch for lp:~adeuring/launchpad/product-sharing-sec-adapter.

The previous branch changed the security adapters required to access attributes of IProduct but it did not include an important detail: If a user has a policy grant for the private product.

This branch fixes this problem.

tests:

./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_userCanView_caches_known_users
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_AnyAllowedPerson_proprietary_product
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_View_.*_product

no lint

To post a comment you must log in.
Steve Kowalik (stevenk) wrote :

33 + store = Store.of(self)
34 + grants_for_user = store.using(
35 + AccessPolicy,
36 + Join(
37 + AccessPolicyGrant,
38 + And(
39 + AccessPolicyGrant.policy_id == AccessPolicy.id,
40 + AccessPolicyGrant.grantee_id == user.id))).find(
41 + AccessPolicyGrant,
42 + AccessPolicy.product_id == self.id,
43 + AccessPolicy.type == self.information_type)
44 + if grants_for_user.is_empty():
45 + return False

This looks remarkably like IAccessPolicyGrant.find() ? Can you just call into that?

223 + owner = product.owner
227 + self.makeAccessPolicyGrant(policy, grantee=owner)

Surely you can just say grantee=product.owner?

review: Needs Fixing (code)
Abel Deuring (adeuring) wrote :

On 03.10.2012 08:10, Steve Kowalik wrote:
> Review: Needs Fixing code
>
> 33 + store = Store.of(self)
> 34 + grants_for_user = store.using(
> 35 + AccessPolicy,
> 36 + Join(
> 37 + AccessPolicyGrant,
> 38 + And(
> 39 + AccessPolicyGrant.policy_id == AccessPolicy.id,
> 40 + AccessPolicyGrant.grantee_id == user.id))).find(
> 41 + AccessPolicyGrant,
> 42 + AccessPolicy.product_id == self.id,
> 43 + AccessPolicy.type == self.information_type)
> 44 + if grants_for_user.is_empty():
> 45 + return False

Sure, done. I simply forgot that we we have some infrastructure to deal
with accesss grants...

>
> This looks remarkably like IAccessPolicyGrant.find() ? Can you just call into that?
>
> 223 + owner = product.owner
> 227 + self.makeAccessPolicyGrant(policy, grantee=owner)
>
> Surely you can just say grantee=product.owner?

Not directly:

            naked_product.information_type = information_type
            if information_type in PROPRIETARY_INFORMATION_TYPES:
                policy = self.makeAccessPolicy(product,
                                               information_type)
                self.makeAccessPolicyGrant(
                    policy, grantee=naked_product.owner)

The change of information_type in the first line above lets userCanView
check if the current user has a grant when the attribute "owner" of
product is retrieved in the last line. But najed_product works of course.

Steve Kowalik (stevenk) :
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/answers/tests/test_question_webservice.py'
2--- lib/lp/answers/tests/test_question_webservice.py 2012-01-01 02:58:52 +0000
3+++ lib/lp/answers/tests/test_question_webservice.py 2012-10-04 10:27:37 +0000
4@@ -10,6 +10,7 @@
5 from simplejson import dumps
6 import transaction
7 from zope.component import getUtility
8+from zope.security.proxy import removeSecurityProxy
9
10 from lp.answers.errors import (
11 AddAnswerContactError,
12@@ -83,6 +84,7 @@
13 with celebrity_logged_in('admin'):
14 self.question = self.factory.makeQuestion(
15 title="This is a question")
16+ self.target_name = self.question.target.name
17
18 self.webservice = LaunchpadWebServiceCaller(
19 'launchpad-library', 'salgado-change-anything')
20@@ -105,7 +107,7 @@
21 def test_GET_xhtml_representation(self):
22 # A question's xhtml representation is available on the api.
23 response = self.webservice.get(
24- '/%s/+question/%d' % (self.question.target.name,
25+ '/%s/+question/%d' % (self.target_name,
26 self.question.id),
27 'application/xhtml+xml')
28 self.assertEqual(response.status, 200)
29@@ -119,7 +121,7 @@
30 new_title = "No, this is a question"
31
32 question_json = self.webservice.get(
33- '/%s/+question/%d' % (self.question.target.name,
34+ '/%s/+question/%d' % (self.target_name,
35 self.question.id)).jsonBody()
36
37 response = self.webservice.patch(
38@@ -156,7 +158,7 @@
39 # End any open lplib instance.
40 logout()
41 lp = launchpadlib_for("test", user)
42- return ws_object(lp, self.question)
43+ return ws_object(lp, removeSecurityProxy(self.question))
44
45 def _set_visibility(self, question):
46 """Method to set visibility; needed for assertRaises."""
47
48=== modified file 'lib/lp/app/model/launchpad.py'
49--- lib/lp/app/model/launchpad.py 2012-10-02 18:44:49 +0000
50+++ lib/lp/app/model/launchpad.py 2012-10-04 10:27:37 +0000
51@@ -43,7 +43,7 @@
52
53
54 class InformationTypeMixin:
55- """"Common functionality for classes implementing IInformationType."""
56+ """Common functionality for classes implementing IInformationType."""
57
58 @property
59 def private(self):
60
61=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
62--- lib/lp/blueprints/browser/tests/test_specification.py 2012-09-25 18:57:52 +0000
63+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-10-04 10:27:37 +0000
64@@ -40,6 +40,7 @@
65 IProductSeries,
66 )
67 from lp.services.features.testing import FeatureFixture
68+from lp.services.webapp.interaction import ANONYMOUS
69 from lp.services.webapp.interfaces import BrowserNotificationLevel
70 from lp.services.webapp.publisher import canonical_url
71 from lp.testing import (
72@@ -467,7 +468,11 @@
73 control = browser.getControl(information_type.title)
74 if not control.selected:
75 control.click()
76- return product.getSpecification(self.submitSpec(browser))
77+ specification_name = self.submitSpec(browser)
78+ # Using the browser terminated the interaction, but we need
79+ # an interaction in order to access a product.
80+ with person_logged_in(ANONYMOUS):
81+ return product.getSpecification(specification_name)
82
83 def test_supplied_information_types(self):
84 """Creating honours information types."""
85@@ -551,9 +556,11 @@
86 Useful because we need to follow to product from a
87 ProductSeries.
88 """
89- if IProductSeries.providedBy(target):
90- return target.product.getSpecification(name)
91- return target.getSpecification(name)
92+ # We need an interaction in order to access a product.
93+ with person_logged_in(ANONYMOUS):
94+ if IProductSeries.providedBy(target):
95+ return target.product.getSpecification(name)
96+ return target.getSpecification(name)
97
98 def submitSpec(self, browser):
99 """Submit a Specification via a browser."""
100
101=== modified file 'lib/lp/blueprints/browser/tests/test_views.py'
102--- lib/lp/blueprints/browser/tests/test_views.py 2012-06-04 16:13:51 +0000
103+++ lib/lp/blueprints/browser/tests/test_views.py 2012-10-04 10:27:37 +0000
104@@ -66,9 +66,9 @@
105 collector = QueryCollector()
106 collector.register()
107 self.addCleanup(collector.unregister)
108+ url = canonical_url(target) + "/+assignments"
109 viewer = self.factory.makePerson()
110 browser = self.getUserBrowser(user=viewer)
111- url = canonical_url(target) + "/+assignments"
112 # Seed the cookie cache and any other cross-request state we may gain
113 # in future. See lp.services.webapp.serssion: _get_secret.
114 browser.open(url)
115
116=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
117--- lib/lp/blueprints/tests/test_webservice.py 2012-08-20 16:38:10 +0000
118+++ lib/lp/blueprints/tests/test_webservice.py 2012-10-04 10:27:37 +0000
119@@ -7,6 +7,7 @@
120
121 import transaction
122 from zope.security.management import endInteraction
123+from zope.security.proxy import removeSecurityProxy
124
125 from lp.blueprints.enums import SpecificationDefinitionStatus
126 from lp.services.webapp.interaction import ANONYMOUS
127@@ -42,8 +43,9 @@
128 return result
129
130 def getPillarOnWebservice(self, pillar_obj):
131+ pillar_name = pillar_obj.name
132 launchpadlib = self.getLaunchpadlib()
133- return launchpadlib.load(pillar_obj.name)
134+ return launchpadlib.load(pillar_name)
135
136
137 class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
138@@ -74,9 +76,9 @@
139 def test_representation_contains_target(self):
140 spec = self.factory.makeSpecification(
141 product=self.factory.makeProduct())
142- spec_target = spec.target
143+ spec_target_name = spec.target.name
144 spec_webservice = self.getSpecOnWebservice(spec)
145- self.assertEqual(spec_target.name, spec_webservice.target.name)
146+ self.assertEqual(spec_target_name, spec_webservice.target.name)
147
148 def test_representation_contains_title(self):
149 spec = self.factory.makeSpecification(title='Foo')
150@@ -271,7 +273,7 @@
151 # setup a new one.
152 endInteraction()
153 lplib = launchpadlib_for('lplib-test', person=None, version='devel')
154- ws_product = ws_object(lplib, product)
155+ ws_product = ws_object(lplib, removeSecurityProxy(product))
156 self.assertNamesOfSpecificationsAre(
157 ["spec1", "spec2"], ws_product.all_specifications)
158
159
160=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
161--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-02 23:45:24 +0000
162+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-04 10:27:37 +0000
163@@ -2005,8 +2005,10 @@
164
165 def test_rendered_query_counts_constant_with_many_bugtasks(self):
166 product = self.factory.makeProduct()
167+ url = canonical_url(product, view_name='+bugs')
168 bug = self.factory.makeBug(target=product)
169 buggy_product = self.factory.makeProduct()
170+ buggy_url = canonical_url(buggy_product, view_name='+bugs')
171 for _ in range(10):
172 self.factory.makeBug(target=buggy_product)
173 recorder = QueryCollector()
174@@ -2014,11 +2016,9 @@
175 self.addCleanup(recorder.unregister)
176 self.invalidate_caches(bug)
177 # count with single task
178- url = canonical_url(product, view_name='+bugs')
179 self.getUserBrowser(url)
180 self.assertThat(recorder, HasQueryCount(LessThan(32)))
181 # count with many tasks
182- buggy_url = canonical_url(buggy_product, view_name='+bugs')
183 self.getUserBrowser(buggy_url)
184 self.assertThat(recorder, HasQueryCount(LessThan(32)))
185
186
187=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt'
188--- lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2012-09-18 18:36:09 +0000
189+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2012-10-04 10:27:37 +0000
190@@ -219,6 +219,7 @@
191 >>> other_product = factory.makeProduct(
192 ... official_malone=True,
193 ... bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
194+ >>> other_product_name = other_product.name
195 >>> params = CreateBugParams(
196 ... title="a test private bug",
197 ... comment="a description of the bug",
198@@ -229,7 +230,7 @@
199
200 >>> browser.open(canonical_url(private_bug, rootsite='bugs'))
201 >>> browser.getLink(url='+choose-affected-product').click()
202- >>> browser.getControl(name='field.product').value = other_product.name
203+ >>> browser.getControl(name='field.product').value = other_product_name
204 >>> browser.getControl('Continue').click()
205 >>> print browser.url
206 http://bugs.launchpad.dev/proprietary-product/+bug/16/+choose-affected-product
207
208=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt'
209--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt 2012-02-24 04:00:24 +0000
210+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt 2012-10-04 10:27:37 +0000
211@@ -9,8 +9,9 @@
212 >>> login(USER_EMAIL)
213 >>> bug = factory.makeBug()
214 >>> task = bug.default_bugtask
215+ >>> url = canonical_url(task, view_name='+subscribe')
216 >>> logout()
217- >>> user_browser.open(canonical_url(task, view_name='+subscribe'))
218+ >>> user_browser.open(url)
219 >>> bug_notification_level_control = user_browser.getControl(
220 ... name='field.bug_notification_level')
221 >>> for control in bug_notification_level_control.controls:
222
223=== modified file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
224--- lib/lp/bugs/stories/patches-view/patches-view.txt 2012-08-08 11:48:29 +0000
225+++ lib/lp/bugs/stories/patches-view/patches-view.txt 2012-10-04 10:27:37 +0000
226@@ -255,9 +255,11 @@
227 ... bugtask.transitionToImportance(importance, ubuntu_distro.owner)
228 ... if status is not None:
229 ... bugtask.transitionToStatus(status, ubuntu_distro.owner)
230+ >>> login(ANONYMOUS)
231 >>> patchy_product_series = patchy_product.getSeries('trunk')
232 >>> make_bugtask(bug=bug_a, target=patchy_product_series)
233 >>> make_bugtask(bug=bug_c, target=patchy_product_series)
234+ >>> logout()
235 >>> anon_browser.open(
236 ... 'https://bugs.launchpad.dev/patchy-product-1/trunk/+patches')
237 >>> show_patches_view(anon_browser.contents)
238
239=== modified file 'lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt'
240--- lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt 2011-12-24 17:49:30 +0000
241+++ lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt 2012-10-04 10:27:37 +0000
242@@ -8,8 +8,8 @@
243 >>> from lp.testing.sampledata import USER_EMAIL
244 >>> login(USER_EMAIL)
245 >>> product = factory.makeProduct()
246+ >>> url = canonical_url(product, view_name='+subscriptions')
247 >>> logout()
248- >>> user_browser.open(
249- ... canonical_url(product, view_name='+subscriptions'))
250+ >>> user_browser.open(url)
251 >>> user_browser.getLink("Add a subscription")
252 <Link text='Add a subscription' url='.../+subscriptions#'>
253
254=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
255--- lib/lp/bugs/stories/webservice/xx-bug.txt 2012-09-27 01:43:05 +0000
256+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2012-10-04 10:27:37 +0000
257@@ -1463,13 +1463,14 @@
258 >>> from lp.testing.sampledata import ADMIN_EMAIL
259 >>> login(ADMIN_EMAIL)
260 >>> target = factory.makeProduct()
261+ >>> target_name = target.name
262 >>> bug = factory.makeBug(target=target)
263 >>> bug = removeSecurityProxy(bug)
264 >>> date = bug.date_last_updated - timedelta(days=6)
265 >>> logout()
266
267 >>> pprint_collection(webservice.named_get(
268- ... '/%s' % target.name, 'searchTasks',
269+ ... '/%s' % target_name, 'searchTasks',
270 ... modified_since=u'%s' % date ).jsonBody())
271 start: 0
272 total_size: 1
273@@ -1481,12 +1482,13 @@
274 >>> from lp.bugs.interfaces.bugtarget import IBugTarget
275 >>> login(ADMIN_EMAIL)
276 >>> target = IBugTarget(factory.makeProduct())
277+ >>> target_name = target.name
278 >>> task = factory.makeBugTask(target=target)
279 >>> date = task.datecreated - timedelta(days=8)
280 >>> logout()
281
282 >>> pprint_collection(webservice.named_get(
283- ... '/%s' % target.name, 'searchTasks',
284+ ... '/%s' % target_name, 'searchTasks',
285 ... created_since=u'%s' % date).jsonBody())
286 start: 0
287 total_size: 1
288@@ -1497,7 +1499,7 @@
289
290 >>> before_date = task.datecreated + timedelta(days=8)
291 >>> pprint_collection(webservice.named_get(
292- ... '/%s' % target.name, 'searchTasks',
293+ ... '/%s' % target_name, 'searchTasks',
294 ... created_before=u'%s' % before_date).jsonBody())
295 start: 0
296 total_size: 1
297
298=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
299--- lib/lp/bugs/tests/test_bugs_webservice.py 2012-09-19 01:19:35 +0000
300+++ lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-04 10:27:37 +0000
301@@ -355,12 +355,13 @@
302 def test_add_duplicate_bugtask_for_project_gives_bad_request(self):
303 bug = self.factory.makeBug()
304 product = self.factory.makeProduct()
305+ product_url = api_url(product)
306 self.factory.makeBugTask(bug=bug, target=product)
307
308 launchpad = launchpadlib_for('test', bug.owner)
309 lp_bug = launchpad.load(api_url(bug))
310 self.assertRaises(
311- BadRequest, lp_bug.addTask, target=api_url(product))
312+ BadRequest, lp_bug.addTask, target=product_url)
313
314 def test_add_invalid_bugtask_to_proprietary_bug_gives_bad_request(self):
315 # Test we get an error when we attempt to invalidly add a bug task to
316@@ -371,6 +372,7 @@
317 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
318 product2 = self.factory.makeProduct(
319 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
320+ product2_url = api_url(product2)
321 bug = self.factory.makeBug(
322 target=product1, owner=owner,
323 information_type=InformationType.PROPRIETARY)
324@@ -379,7 +381,7 @@
325 launchpad = launchpadlib_for('test', owner)
326 lp_bug = launchpad.load(api_url(bug))
327 self.assertRaises(
328- BadRequest, lp_bug.addTask, target=api_url(product2))
329+ BadRequest, lp_bug.addTask, target=product2_url)
330
331 def test_add_attachment_with_bad_filename_raises_exception(self):
332 # Test that addAttachment raises BadRequest when the filename given
333@@ -404,12 +406,13 @@
334 # to the project's bugs are private by default rule.
335 project = self.factory.makeLegacyProduct(
336 licenses=[License.OTHER_PROPRIETARY])
337+ target_url = api_url(project)
338 with person_logged_in(project.owner):
339 project.setPrivateBugs(True, project.owner)
340 webservice = launchpadlib_for('test', 'salgado')
341 bugs_collection = webservice.load('/bugs')
342 bug = bugs_collection.createBug(
343- target=api_url(project), title='title', description='desc')
344+ target=target_url, title='title', description='desc')
345 self.assertEqual('Private', bug.information_type)
346
347 def test_explicit_private_private_bugs_true(self):
348@@ -418,12 +421,13 @@
349 # user commands it.
350 project = self.factory.makeLegacyProduct(
351 licenses=[License.OTHER_PROPRIETARY])
352+ target_url = api_url(project)
353 with person_logged_in(project.owner):
354 project.setPrivateBugs(True, project.owner)
355 webservice = launchpadlib_for('test', 'salgado')
356 bugs_collection = webservice.load('/bugs')
357 bug = bugs_collection.createBug(
358- target=api_url(project), title='title', description='desc',
359+ target=target_url, title='title', description='desc',
360 private=True)
361 self.assertEqual('Private', bug.information_type)
362
363@@ -433,13 +437,14 @@
364 # to the project's bug sharing policy.
365 project = self.factory.makeProduct(
366 licenses=[License.OTHER_PROPRIETARY])
367+ target_url = api_url(project)
368 with person_logged_in(project.owner):
369 project.setBugSharingPolicy(
370 BugSharingPolicy.PROPRIETARY_OR_PUBLIC)
371 webservice = launchpadlib_for('test', 'salgado')
372 bugs_collection = webservice.load('/bugs')
373 bug = bugs_collection.createBug(
374- target=api_url(project), title='title', description='desc')
375+ target=target_url, title='title', description='desc')
376 self.assertEqual('Proprietary', bug.information_type)
377
378
379@@ -459,7 +464,7 @@
380
381 def test_subscribe_does_not_update(self):
382 # Calling subscribe over the API does not update date_last_updated.
383- (bug, owner, webservice) = self.make_old_bug()
384+ (bug, owner, webservice) = self.make_old_bug()
385 subscriber = self.factory.makePerson()
386 date_last_updated = bug.date_last_updated
387 api_sub = api_url(subscriber)
388
389=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
390--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-09-21 02:03:56 +0000
391+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-10-04 10:27:37 +0000
392@@ -54,6 +54,7 @@
393 self.owner = self.factory.makePerson()
394 with person_logged_in(self.owner):
395 self.product = self.factory.makeProduct()
396+ self.product_name = self.product.name
397 self.bug = self.factory.makeBug(
398 target=self.product,
399 information_type=InformationType.PRIVATESECURITY)
400@@ -62,7 +63,7 @@
401
402 def search(self, api_version, **kwargs):
403 return self.webservice.named_get(
404- '/%s' % self.product.name, 'searchTasks',
405+ '/%s' % self.product_name, 'searchTasks',
406 api_version=api_version, **kwargs).jsonBody()
407
408 def test_linked_blueprints_in_devel(self):
409@@ -102,7 +103,7 @@
410 def test_search_with_wrong_orderby(self):
411 # Calling searchTasks() with a wrong order_by is a Bad Request.
412 response = self.webservice.named_get(
413- '/%s' % self.product.name, 'searchTasks',
414+ '/%s' % self.product_name, 'searchTasks',
415 api_version='devel', order_by='date_created')
416 self.assertEqual(400, response.status)
417 self.assertRaisesWithContent(
418
419=== modified file 'lib/lp/code/browser/tests/test_branch.py'
420--- lib/lp/code/browser/tests/test_branch.py 2012-09-20 21:26:42 +0000
421+++ lib/lp/code/browser/tests/test_branch.py 2012-10-04 10:27:37 +0000
422@@ -698,11 +698,11 @@
423 base_url = canonical_url(branch, rootsite='code')
424 product_url = canonical_url(product, rootsite='code')
425 url = '%s/+subscription/%s' % (base_url, subscriber.name)
426+ expected_title = "Code : %s" % product.displayname
427 browser = self._getBrowser(user=subscriber)
428 browser.open(url)
429 browser.getControl('Unsubscribe').click()
430 self.assertEqual(product_url, browser.url)
431- expected_title = "Code : %s" % product.displayname
432 self.assertEqual(expected_title, browser.title)
433
434
435
436=== modified file 'lib/lp/code/browser/tests/test_product.py'
437--- lib/lp/code/browser/tests/test_product.py 2012-10-03 03:56:29 +0000
438+++ lib/lp/code/browser/tests/test_product.py 2012-10-04 10:27:37 +0000
439@@ -224,8 +224,9 @@
440 login_person(product.owner)
441 product.development_focus.branch = code_import.branch
442 self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)
443+ product_url = canonical_url(product, rootsite='code')
444 logout()
445- browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
446+ browser = self.getUserBrowser(product_url)
447 login(ANONYMOUS)
448 content = find_tag_by_id(browser.contents, 'external')
449 text = extract_text(content)
450@@ -383,6 +384,7 @@
451
452 def test_is_public(self):
453 product = self.factory.makeProduct()
454+ product_displayname = product.displayname
455 branch = self.factory.makeProductBranch(product=product)
456 login_person(product.owner)
457 product.development_focus.branch = branch
458@@ -390,7 +392,7 @@
459 text = extract_text(find_tag_by_id(browser.contents, 'privacy'))
460 expected = (
461 "New branches for %(name)s are Public.*"
462- % dict(name=product.displayname))
463+ % dict(name=product_displayname))
464 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
465
466
467
468=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
469--- lib/lp/code/stories/branches/xx-product-branches.txt 2012-04-10 14:01:17 +0000
470+++ lib/lp/code/stories/branches/xx-product-branches.txt 2012-10-04 10:27:37 +0000
471@@ -181,10 +181,10 @@
472 >>> factory = LaunchpadObjectFactory()
473 >>> login(ANONYMOUS)
474 >>> product = getUtility(IProductSet).getByName('firefox')
475+ >>> owner = product.owner
476 >>> old_branch = product.development_focus.branch
477 >>> ignored = login_person(product.owner)
478 >>> product.development_focus.branch = None
479- >>> logout()
480 >>> def print_links(browser):
481 ... links = find_tag_by_id(browser.contents, 'involvement')
482 ... if links is None:
483@@ -204,13 +204,16 @@
484
485 >>> print product.codehosting_usage.name
486 UNKNOWN
487+ >>> logout()
488 >>> admin_browser.open('http://code.launchpad.dev/firefox')
489 >>> print_links(admin_browser)
490 None
491
492 >>> setup_code_hosting('firefox')
493+ >>> login(ANONYMOUS)
494 >>> print product.codehosting_usage.name
495 LAUNCHPAD
496+ >>> logout()
497 >>> admin_browser.open('http://code.launchpad.dev/firefox')
498 >>> print_links(admin_browser)
499 Import a branch
500@@ -235,7 +238,7 @@
501 If the product specifies that it officially uses Launchpad code, then
502 the 'Import a branch' button is still shown.
503
504- >>> ignored = login_person(product.owner)
505+ >>> ignored = login_person(owner)
506 >>> product.development_focus.branch = old_branch
507 >>> logout()
508 >>> browser.open('http://code.launchpad.dev/firefox')
509
510=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
511--- lib/lp/code/stories/webservice/xx-code-import.txt 2011-12-24 17:49:30 +0000
512+++ lib/lp/code/stories/webservice/xx-code-import.txt 2012-10-04 10:27:37 +0000
513@@ -17,6 +17,7 @@
514 >>> other_person = factory.makePerson(name='other-person')
515 >>> removeSecurityProxy(person).join(team)
516 >>> product = factory.makeProduct(name='scruff')
517+ >>> product_name = product.name
518 >>> svn_branch_url = "http://svn.domain.com/source"
519 >>> code_import = removeSecurityProxy(factory.makeProductCodeImport(
520 ... registrant=person, product=product, branch_name='import',
521@@ -122,7 +123,7 @@
522
523 We can create an import using the API by calling a method on the project.
524
525- >>> product_url = '/' + product.name
526+ >>> product_url = '/' + product_name
527 >>> new_remote_url = factory.getUniqueURL()
528 >>> response = import_webservice.named_post(product_url, 'newCodeImport',
529 ... branch_name='new-import', rcs_type='Git',
530@@ -149,7 +150,7 @@
531
532 If we must we can create a CVS import.
533
534- >>> product_url = '/' + product.name
535+ >>> product_url = '/' + product_name
536 >>> new_remote_url = factory.getUniqueURL()
537 >>> response = import_webservice.named_post(product_url, 'newCodeImport',
538 ... branch_name='cvs-import', rcs_type='Concurrent Versions System',
539
540=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
541--- lib/lp/registry/browser/tests/test_milestone.py 2012-09-28 02:42:20 +0000
542+++ lib/lp/registry/browser/tests/test_milestone.py 2012-10-04 10:27:37 +0000
543@@ -234,12 +234,13 @@
544 super(TestProjectMilestoneIndexQueryCount, self).setUp()
545 self.owner = self.factory.makePerson(name='product-owner')
546 self.product = self.factory.makeProduct(owner=self.owner)
547+ self.product_owner = self.product.owner
548 login_person(self.product.owner)
549 self.milestone = self.factory.makeMilestone(
550 productseries=self.product.development_focus)
551
552 def add_bug(self, count):
553- login_person(self.product.owner)
554+ login_person(self.product_owner)
555 for i in range(count):
556 bug = self.factory.makeBug(target=self.product)
557 bug.bugtasks[0].transitionToMilestone(
558@@ -284,6 +285,7 @@
559 # increasing the cap.
560 page_query_limit = 37
561 product = self.factory.makeProduct()
562+ product_owner = product.owner
563 login_person(product.owner)
564 milestone = self.factory.makeMilestone(
565 productseries=product.development_focus)
566@@ -316,7 +318,7 @@
567 with_1_private_bug = collector.count
568 with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in
569 enumerate(collector.queries)]
570- login_person(product.owner)
571+ login_person(product_owner)
572 bug2 = self.factory.makeBug(
573 target=product, information_type=InformationType.USERDATA,
574 owner=product.owner)
575
576=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
577--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-09-18 19:41:02 +0000
578+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-10-04 10:27:37 +0000
579@@ -243,10 +243,10 @@
580
581 def test_sharing_menu(self):
582 url = canonical_url(self.pillar)
583- browser = setupBrowserForUser(user=self.driver)
584- browser.open(url)
585- soup = BeautifulSoup(browser.contents)
586 sharing_url = canonical_url(self.pillar, view_name='+sharing')
587+ browser = setupBrowserForUser(user=self.driver)
588+ browser.open(url)
589+ soup = BeautifulSoup(browser.contents)
590 sharing_menu = soup.find('a', {'href': sharing_url})
591 self.assertIsNotNone(sharing_menu)
592
593
594=== modified file 'lib/lp/registry/browser/tests/test_product.py'
595--- lib/lp/registry/browser/tests/test_product.py 2012-09-10 13:24:03 +0000
596+++ lib/lp/registry/browser/tests/test_product.py 2012-10-04 10:27:37 +0000
597@@ -449,8 +449,8 @@
598 def test_headers(self):
599 """The headers for the RDF view of a product should be as expected."""
600 product = self.factory.makeProduct()
601+ content_disposition = 'attachment; filename="%s.rdf"' % product.name
602 browser = self.getViewBrowser(product, view_name='+rdf')
603- content_disposition = 'attachment; filename="%s.rdf"' % product.name
604 self.assertEqual(
605 content_disposition, browser.headers['Content-disposition'])
606 self.assertEqual(
607
608=== modified file 'lib/lp/registry/configure.zcml'
609--- lib/lp/registry/configure.zcml 2012-10-04 10:27:37 +0000
610+++ lib/lp/registry/configure.zcml 2012-10-04 10:27:37 +0000
611@@ -1229,6 +1229,8 @@
612 class="lp.registry.model.product.Product">
613 <allow
614 interface="lp.registry.interfaces.product.IProductPublic"/>
615+ <allow
616+ interface="lp.registry.interfaces.pillar.IPillar"/>
617 <require
618 permission="launchpad.View"
619 interface="lp.registry.interfaces.product.IProductLimitedView"/>
620
621=== modified file 'lib/lp/registry/interfaces/product.py'
622--- lib/lp/registry/interfaces/product.py 2012-10-04 10:27:37 +0000
623+++ lib/lp/registry/interfaces/product.py 2012-10-04 10:27:37 +0000
624@@ -426,13 +426,6 @@
625
626 id = Int(title=_('The Project ID'))
627
628- information_type = exported(
629- Choice(
630- title=_('Information Type'), vocabulary=InformationType,
631- required=True, readonly=True,
632- description=_(
633- 'The type of of data contained in this project.')))
634-
635 def userCanView(user):
636 """True if the given user has access to this product."""
637
638@@ -443,7 +436,7 @@
639 IHasLogo, IHasMergeProposals, IHasMilestones,
640 IHasMugshot, IHasOwner, IHasSprints, IHasTranslationImports,
641 ITranslationPolicy, IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
642- IOfficialBugTagTargetPublic, IHasOOPSReferences, IPillar,
643+ IOfficialBugTagTargetPublic, IHasOOPSReferences,
644 ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):
645 """Public IProduct properties."""
646
647@@ -938,13 +931,8 @@
648 class IProduct(
649 IHasBugSupervisor, IProductEditRestricted,
650 IProductModerateRestricted, IProductDriverRestricted,
651-<<<<<<< TREE
652- IProductPublic, IQuestionTarget, IRootContext,
653- IStructuralSubscriptionTarget, IInformationType):
654-=======
655 IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
656- IStructuralSubscriptionTarget):
657->>>>>>> MERGE-SOURCE
658+ IStructuralSubscriptionTarget, IInformationType, IPillar):
659 """A Product.
660
661 The Launchpad Registry describes the open source world as ProjectGroups
662
663=== modified file 'lib/lp/registry/model/product.py'
664--- lib/lp/registry/model/product.py 2012-10-04 10:27:37 +0000
665+++ lib/lp/registry/model/product.py 2012-10-04 10:27:37 +0000
666@@ -69,12 +69,9 @@
667 FREE_INFORMATION_TYPES,
668 InformationType,
669 PRIVATE_INFORMATION_TYPES,
670-<<<<<<< TREE
671+ PUBLIC_INFORMATION_TYPES,
672 PUBLIC_PROPRIETARY_INFORMATION_TYPES,
673 PROPRIETARY_INFORMATION_TYPES,
674-=======
675- PUBLIC_INFORMATION_TYPES,
676->>>>>>> MERGE-SOURCE
677 service_uses_launchpad,
678 ServiceUsage,
679 )
680@@ -424,7 +421,6 @@
681 """
682 pass
683
684-<<<<<<< TREE
685 def _valid_product_information_type(self, attr, value):
686 if value not in PUBLIC_PROPRIETARY_INFORMATION_TYPES:
687 raise CannotChangeInformationType('Not supported for Projects.')
688@@ -438,22 +434,18 @@
689 ' Projects.')
690 return value
691
692- information_type = EnumCol(
693+ _information_type = EnumCol(
694 enum=InformationType, default=InformationType.PUBLIC,
695+ dbName='information_type',
696 storm_validator=_valid_product_information_type)
697-=======
698- # Place holder for a db column.
699- # XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
700- # into place.
701+
702 def _get_information_type(self):
703- return getattr(
704- self, '_information_type', InformationType.PUBLIC)
705+ return self._information_type or InformationType.PUBLIC
706
707 def _set_information_type(self, value):
708 self._information_type = value
709
710 information_type = property(_get_information_type, _set_information_type)
711->>>>>>> MERGE-SOURCE
712
713 security_contact = None
714
715@@ -1550,11 +1542,30 @@
716
717 return weight_function
718
719+ @cachedproperty
720+ def _known_viewers(self):
721+ """A set of known persons able to view this product."""
722+ return set()
723+
724 def userCanView(self, user):
725 """See `IProductPublic`."""
726 if self.information_type in PUBLIC_INFORMATION_TYPES:
727 return True
728- return user.inTeam(self.owner)
729+ if user is None:
730+ return False
731+ if user.id in self._known_viewers:
732+ return True
733+ # We want an actual Storm Person.
734+ if IPersonRoles.providedBy(user):
735+ user = user.person
736+ policy = getUtility(IAccessPolicySource).find(
737+ [(self, self.information_type)]).one()
738+ grants_for_user = getUtility(IAccessPolicyGrantSource).find(
739+ [(policy, user)])
740+ if grants_for_user.is_empty():
741+ return False
742+ self._known_viewers.add(user.id)
743+ return True
744
745
746 class ProductSet:
747
748=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
749--- lib/lp/registry/services/tests/test_sharingservice.py 2012-09-27 01:30:56 +0000
750+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-10-04 10:27:37 +0000
751@@ -1838,12 +1838,14 @@
752 api_method, api_version='devel', **kwargs).jsonBody()
753
754 def _getPillarGranteeData(self):
755- pillar_uri = canonical_url(self.pillar, force_local_path=True)
756+ pillar_uri = canonical_url(
757+ removeSecurityProxy(self.pillar), force_local_path=True)
758 return self._named_get(
759 'getPillarGranteeData', pillar=pillar_uri)
760
761 def _sharePillarInformation(self, pillar):
762- pillar_uri = canonical_url(pillar, force_local_path=True)
763+ pillar_uri = canonical_url(
764+ removeSecurityProxy(pillar), force_local_path=True)
765 return self._named_post(
766 'sharePillarInformation', pillar=pillar_uri,
767 grantee=self.grantee_uri,
768
769=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
770--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-09-25 16:52:09 +0000
771+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-10-04 10:27:37 +0000
772@@ -168,7 +168,7 @@
773 freshmeat_project: None
774 homepage_url: None
775 icon_link: u'http://.../firefox/icon'
776- information_type: None
777+ information_type: u'Public'
778 is_permitted: True
779 license_approved: False
780 license_info: None
781
782=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
783--- lib/lp/registry/tests/test_pillaraffiliation.py 2012-09-10 13:24:03 +0000
784+++ lib/lp/registry/tests/test_pillaraffiliation.py 2012-10-04 10:27:37 +0000
785@@ -150,7 +150,7 @@
786 Store.of(product).invalidate()
787 with StormStatementRecorder() as recorder:
788 IHasAffiliation(product).getAffiliationBadges([person])
789- self.assertThat(recorder, HasQueryCount(Equals(5)))
790+ self.assertThat(recorder, HasQueryCount(Equals(4)))
791
792 def test_distro_affiliation_query_count(self):
793 # Only 2 business queries are expected, selects from:
794
795=== modified file 'lib/lp/registry/tests/test_product.py'
796--- lib/lp/registry/tests/test_product.py 2012-10-04 10:27:37 +0000
797+++ lib/lp/registry/tests/test_product.py 2012-10-04 10:27:37 +0000
798@@ -45,6 +45,7 @@
799 BugSharingPolicy,
800 EXCLUSIVE_TEAM_POLICY,
801 INCLUSIVE_TEAM_POLICY,
802+ SharingPermission,
803 SpecificationSharingPolicy,
804 )
805 from lp.registry.errors import (
806@@ -73,6 +74,7 @@
807 celebrity_logged_in,
808 login,
809 person_logged_in,
810+ StormStatementRecorder,
811 TestCase,
812 TestCaseWithFactory,
813 WebServiceTestCase,
814@@ -401,7 +403,6 @@
815 expected = [InformationType.PROPRIETARY]
816 self.assertContentEqual(expected, [policy.type for policy in aps])
817
818-<<<<<<< TREE
819 def createProduct(self, information_type=None, license=None):
820 # convenience method for testing IProductSet.createProduct rather than
821 # self.factory.makeProduct
822@@ -423,7 +424,7 @@
823 license=License.OTHER_PROPRIETARY)
824 self.assertEqual(InformationType.EMBARGOED, product.information_type)
825 # Owner can set information_type
826- with person_logged_in(product.owner):
827+ with person_logged_in(removeSecurityProxy(product).owner):
828 product.information_type = InformationType.PROPRIETARY
829 self.assertEqual(InformationType.PROPRIETARY, product.information_type)
830 # Database persists information_type value
831@@ -435,7 +436,9 @@
832
833 def test_product_information_type_default(self):
834 # Default information_type is PUBLIC
835- product = self.createProduct()
836+ owner = self.factory.makePerson()
837+ product = getUtility(IProductSet).createProduct(
838+ owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2')
839 self.assertEqual(InformationType.PUBLIC, product.information_type)
840
841 invalid_information_types = [info_type for info_type in
842@@ -487,7 +490,6 @@
843 product = self.createProduct(info_type, License.OTHER_PROPRIETARY)
844 self.assertEqual(info_type, product.information_type)
845
846-=======
847 def check_permissions(self, expected_permissions, used_permissions,
848 type_):
849 expected = set(expected_permissions.keys())
850@@ -511,10 +513,13 @@
851 expected_permissions[permission] - attribute_names)))
852
853 expected_get_permissions = {
854- CheckerPublic: set(('id', 'information_type', 'userCanView',)),
855+ CheckerPublic: set((
856+ 'active', 'id', 'information_type', 'pillar_category', 'private',
857+ 'userCanView',)),
858 'launchpad.View': set((
859- '_getOfficialTagClause', 'active', 'active_or_packaged_series',
860- 'aliases', 'all_milestones', 'all_specifications',
861+ '_getOfficialTagClause', '_all_specifications',
862+ '_valid_specifications', 'active_or_packaged_series',
863+ 'aliases', 'all_milestones',
864 'allowsTranslationEdits', 'allowsTranslationSuggestions',
865 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
866 'blueprints_usage', 'branch_sharing_policy',
867@@ -551,13 +556,12 @@
868 'getTopContributors', 'getTopContributorsGroupedByCategory',
869 'getTranslationGroups', 'getTranslationImportQueueEntries',
870 'getTranslators', 'getUsedBugTagsWithOpenCounts',
871- 'getVersionSortedSeries', 'has_any_specifications',
872+ 'getVersionSortedSeries',
873 'has_current_commercial_subscription',
874 'has_custom_language_codes', 'has_milestones', 'homepage_content',
875 'homepageurl', 'icon', 'invitesTranslationEdits',
876 'invitesTranslationSuggestions',
877 'isUsingInheritedBranchVisibilityPolicy',
878- 'latest_completed_specifications', 'latest_specifications',
879 'license_info', 'license_status', 'licenses', 'logo', 'milestones',
880 'mugshot', 'name', 'name_with_project', 'newCodeImport',
881 'obsolete_translatable_series', 'official_answers',
882@@ -565,7 +569,7 @@
883 'official_codehosting', 'official_malone', 'owner',
884 'parent_subscription_target', 'packagedInDistros', 'packagings',
885 'past_sprints', 'personHasDriverRights', 'pillar',
886- 'pillar_category', 'primary_translatable', 'private_bugs',
887+ 'primary_translatable', 'private_bugs',
888 'programminglang', 'project', 'qualifies_for_free_hosting',
889 'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
890 'remote_product', 'removeCustomLanguageCode',
891@@ -580,7 +584,7 @@
892 'translationpermission', 'translations_usage', 'ubuntu_packages',
893 'userCanAlterBugSubscription', 'userCanAlterSubscription',
894 'userCanEdit', 'userHasBugSubscriptions', 'uses_launchpad',
895- 'valid_specifications', 'wikiurl')),
896+ 'wikiurl')),
897 'launchpad.AnyAllowedPerson': set((
898 'addAnswerContact', 'addBugSubscription',
899 'addBugSubscriptionFilter', 'addSubscription',
900@@ -615,9 +619,10 @@
901 'bug_tracking_usage', 'codehosting_usage',
902 'commercial_subscription', 'description', 'development_focus',
903 'displayname', 'downloadurl', 'driver', 'freshmeatproject',
904- 'homepage_content', 'homepageurl', 'icon', 'license_info',
905- 'licenses', 'logo', 'mugshot', 'official_answers',
906- 'official_blueprints', 'official_codehosting', 'owner',
907+ 'homepage_content', 'homepageurl', 'icon', 'information_type',
908+ 'license_info', 'licenses', 'logo', 'mugshot',
909+ 'official_answers', 'official_blueprints',
910+ 'official_codehosting', 'owner', 'private',
911 'programminglang', 'project', 'redeemSubscriptionVoucher',
912 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
913 'summary', 'title', 'uses_launchpad', 'wikiurl')),
914@@ -655,8 +660,10 @@
915 def test_access_launchpad_View_proprietary_product(self):
916 # Only people with grants for a private product can access
917 # attributes protected by the permission launchpad.View.
918- product = self.factory.makeProduct(
919- information_type=InformationType.PROPRIETARY)
920+ product = self.createProduct(
921+ information_type=InformationType.PROPRIETARY,
922+ license=License.OTHER_PROPRIETARY)
923+ owner = removeSecurityProxy(product).owner
924 names = self.expected_get_permissions['launchpad.View']
925 with person_logged_in(None):
926 for attribute_name in names:
927@@ -667,7 +674,16 @@
928 for attribute_name in names:
929 self.assertRaises(
930 Unauthorized, getattr, product, attribute_name)
931- with person_logged_in(removeSecurityProxy(product).owner):
932+ with person_logged_in(owner):
933+ for attribute_name in names:
934+ getattr(product, attribute_name)
935+ # A user with a policy grant for the product can access attributes
936+ # of a private product.
937+ with person_logged_in(owner):
938+ getUtility(IService, 'sharing').sharePillarInformation(
939+ product, ordinary_user, owner,
940+ {InformationType.PROPRIETARY: SharingPermission.ALL})
941+ with person_logged_in(ordinary_user):
942 for attribute_name in names:
943 getattr(product, attribute_name)
944
945@@ -691,8 +707,10 @@
946 def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
947 # Only people with grants for a private product can access
948 # attributes protected by the permission launchpad.AnyAllowedPerson.
949- product = self.factory.makeProduct(
950- information_type=InformationType.PROPRIETARY)
951+ product = self.createProduct(
952+ information_type=InformationType.PROPRIETARY,
953+ license=License.OTHER_PROPRIETARY)
954+ owner = removeSecurityProxy(product).owner
955 names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
956 with person_logged_in(None):
957 for attribute_name in names:
958@@ -703,7 +721,16 @@
959 for attribute_name in names:
960 self.assertRaises(
961 Unauthorized, getattr, product, attribute_name)
962- with person_logged_in(removeSecurityProxy(product).owner):
963+ with person_logged_in(owner):
964+ for attribute_name in names:
965+ getattr(product, attribute_name)
966+ # A user with a policy grant for the product can access attributes
967+ # of a private product.
968+ with person_logged_in(owner):
969+ getUtility(IService, 'sharing').sharePillarInformation(
970+ product, ordinary_user, owner,
971+ {InformationType.PROPRIETARY: SharingPermission.ALL})
972+ with person_logged_in(ordinary_user):
973 for attribute_name in names:
974 getattr(product, attribute_name)
975
976@@ -724,8 +751,10 @@
977 def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
978 # Only people with grants for a private product can set
979 # attributes protected by the permission launchpad.AnyAllowedPerson.
980- product = self.factory.makeProduct(
981- information_type=InformationType.PROPRIETARY)
982+ product = self.createProduct(
983+ information_type=InformationType.PROPRIETARY,
984+ license=License.OTHER_PROPRIETARY)
985+ owner = removeSecurityProxy(product).owner
986 with person_logged_in(None):
987 self.assertRaises(
988 Unauthorized, setattr, product, 'date_next_suggest_packaging',
989@@ -735,10 +764,40 @@
990 self.assertRaises(
991 Unauthorized, setattr, product, 'date_next_suggest_packaging',
992 'foo')
993- with person_logged_in(removeSecurityProxy(product).owner):
994- setattr(product, 'date_next_suggest_packaging', 'foo')
995-
996->>>>>>> MERGE-SOURCE
997+ with person_logged_in(owner):
998+ setattr(product, 'date_next_suggest_packaging', 'foo')
999+ # A user with a policy grant for the product can access attributes
1000+ # of a private product.
1001+ with person_logged_in(owner):
1002+ getUtility(IService, 'sharing').sharePillarInformation(
1003+ product, ordinary_user, owner,
1004+ {InformationType.PROPRIETARY: SharingPermission.ALL})
1005+ with person_logged_in(ordinary_user):
1006+ setattr(product, 'date_next_suggest_packaging', 'foo')
1007+
1008+ def test_userCanView_caches_known_users(self):
1009+ # userCanView() maintains a cache of users known to have the
1010+ # permission to access a product.
1011+ product = self.createProduct(
1012+ information_type=InformationType.PROPRIETARY,
1013+ license=License.OTHER_PROPRIETARY)
1014+ owner = removeSecurityProxy(product).owner
1015+ user = self.factory.makePerson()
1016+ with person_logged_in(owner):
1017+ getUtility(IService, 'sharing').sharePillarInformation(
1018+ product, user, owner,
1019+ {InformationType.PROPRIETARY: SharingPermission.ALL})
1020+ with person_logged_in(user):
1021+ with StormStatementRecorder() as recorder:
1022+ # The first access to a property of the product from
1023+ # a user requires a DB query.
1024+ product.homepageurl
1025+ queries_for_first_user_access = len(recorder.queries)
1026+ # The second access does not require another query.
1027+ product.description
1028+ self.assertEqual(
1029+ queries_for_first_user_access, len(recorder.queries))
1030+
1031
1032 class TestProductBugInformationTypes(TestCaseWithFactory):
1033
1034
1035=== modified file 'lib/lp/registry/tests/test_subscribers.py'
1036--- lib/lp/registry/tests/test_subscribers.py 2012-05-25 21:16:11 +0000
1037+++ lib/lp/registry/tests/test_subscribers.py 2012-10-04 10:27:37 +0000
1038@@ -170,7 +170,10 @@
1039 # If there is no request, there is no reason to show a message in
1040 # the browser.
1041 product, user = self.make_product_user([License.GNU_GPL_V2])
1042- notification = LicenseNotification(product, user)
1043+ # Using the proxied product leads to an exeception when
1044+ # notification.display() below is called because the permission
1045+ # checks product require an interaction.
1046+ notification = LicenseNotification(removeSecurityProxy(product), user)
1047 logout()
1048 result = notification.display()
1049 self.assertIs(False, result)
1050
1051=== modified file 'lib/lp/scripts/garbo.py'
1052--- lib/lp/scripts/garbo.py 2012-10-02 19:25:44 +0000
1053+++ lib/lp/scripts/garbo.py 2012-10-04 10:27:37 +0000
1054@@ -928,10 +928,10 @@
1055 def __call__(self, chunk_size):
1056 """See `TunableLoop`."""
1057 subselect = Select(
1058- Product.id, Product.information_type == None,
1059+ Product.id, Product._information_type == None,
1060 limit=chunk_size)
1061 result = self.store.execute(
1062- Update({Product.information_type: 1},
1063+ Update({Product._information_type: 1},
1064 Product.id.is_in(subselect)))
1065 transaction.commit()
1066 self.rows_updated = result.rowcount
1067
1068=== modified file 'lib/lp/scripts/tests/test_garbo.py'
1069--- lib/lp/scripts/tests/test_garbo.py 2012-10-03 17:41:00 +0000
1070+++ lib/lp/scripts/tests/test_garbo.py 2012-10-04 10:27:37 +0000
1071@@ -1064,18 +1064,18 @@
1072 # Set all existing projects to something other than None or 1.
1073 store = IMasterStore(Product)
1074 store.execute(Update(
1075- {Product.information_type: 2}))
1076+ {Product._information_type: 2}))
1077 store.flush()
1078 # Make a new product without an information_type.
1079 product = self.factory.makeProduct()
1080 store.execute(Update(
1081- {Product.information_type: None}, Product.id == product.id))
1082+ {Product._information_type: None}, Product.id == product.id))
1083 store.flush()
1084 self.assertEqual(1, store.find(Product,
1085- Product.information_type == None).count())
1086+ Product._information_type == None).count())
1087 self.runDaily()
1088 self.assertEqual(0, store.find(Product,
1089- Product.information_type == None).count())
1090+ Product._information_type == None).count())
1091
1092
1093 class TestGarboTasks(TestCaseWithFactory):
1094
1095=== modified file 'lib/lp/security.py'
1096--- lib/lp/security.py 2012-10-04 10:27:37 +0000
1097+++ lib/lp/security.py 2012-10-04 10:27:37 +0000
1098@@ -431,15 +431,19 @@
1099 return user.isOwner(self.obj) or user.in_admin
1100
1101
1102-class ViewProduct(AuthorizationBase):
1103+class ViewProduct(ViewPillar):
1104 permission = 'launchpad.View'
1105 usedfor = IProduct
1106
1107 def checkAuthenticated(self, user):
1108- return self.obj.userCanView(user)
1109+ return (
1110+ super(ViewProduct, self).checkAuthenticated(user) and
1111+ self.obj.userCanView(user))
1112
1113 def checkUnauthenticated(self):
1114- return self.obj.information_type in PUBLIC_INFORMATION_TYPES
1115+ return (
1116+ self.obj.information_type in PUBLIC_INFORMATION_TYPES and
1117+ super(ViewProduct, self).checkUnauthenticated())
1118
1119
1120 class ChangeProduct(ViewProduct):
1121
1122=== modified file 'lib/lp/testing/factory.py'
1123--- lib/lp/testing/factory.py 2012-10-04 10:27:37 +0000
1124+++ lib/lp/testing/factory.py 2012-10-04 10:27:37 +0000
1125@@ -64,6 +64,7 @@
1126
1127 from lp.app.enums import (
1128 InformationType,
1129+ PROPRIETARY_INFORMATION_TYPES,
1130 PUBLIC_INFORMATION_TYPES,
1131 ServiceUsage,
1132 )
1133@@ -1024,6 +1025,10 @@
1134 specification_sharing_policy)
1135 if information_type is not None:
1136 naked_product.information_type = information_type
1137+ if information_type in PROPRIETARY_INFORMATION_TYPES:
1138+ policy = self.makeAccessPolicy(product, information_type)
1139+ self.makeAccessPolicyGrant(
1140+ policy, grantee=naked_product.owner)
1141
1142 return product
1143
1144@@ -1057,7 +1062,7 @@
1145 if product is None:
1146 product = self.makeProduct()
1147 if owner is None:
1148- owner = product.owner
1149+ owner = removeSecurityProxy(product).owner
1150 if name is None:
1151 name = self.getUniqueString()
1152 if summary is None:
1153@@ -1834,7 +1839,8 @@
1154
1155 if owner is None:
1156 owner = self.makePerson()
1157- return removeSecurityProxy(bug).addTask(owner, target)
1158+ return removeSecurityProxy(bug).addTask(
1159+ owner, removeSecurityProxy(target))
1160
1161 def makeBugNomination(self, bug=None, target=None):
1162 """Create and return a BugNomination.
1163
1164=== modified file 'lib/lp/translations/browser/tests/test_noindex.py'
1165--- lib/lp/translations/browser/tests/test_noindex.py 2012-01-01 02:58:52 +0000
1166+++ lib/lp/translations/browser/tests/test_noindex.py 2012-10-04 10:27:37 +0000
1167@@ -46,7 +46,12 @@
1168 # Using create_initialized_view for distroseries causes an error when
1169 # rendering the view due to the way the view is registered and menus
1170 # are adapted. Getting the contents via a browser does work.
1171- self.user_browser.open(self.url)
1172+ #
1173+ # Retrieve the URL before the user_browser is created. Products
1174+ # can only be access with an active interaction, and getUserBrowser()
1175+ # closes the current interaction.
1176+ url = self.url
1177+ self.user_browser.open(url)
1178 return self.user_browser.contents
1179
1180 def getRobotsDirective(self):
1181
1182=== modified file 'lib/lp/translations/stories/importqueue/xx-entry-details.txt'
1183--- lib/lp/translations/stories/importqueue/xx-entry-details.txt 2012-05-24 20:25:54 +0000
1184+++ lib/lp/translations/stories/importqueue/xx-entry-details.txt 2012-10-04 10:27:37 +0000
1185@@ -15,6 +15,7 @@
1186 >>> queue = TranslationImportQueue()
1187 >>> product = factory.makeProduct(
1188 ... translations_usage=ServiceUsage.LAUNCHPAD)
1189+ >>> product_displayname = product.displayname
1190 >>> trunk = product.getSeries('trunk')
1191 >>> uploader = factory.makePerson()
1192 >>> entry = queue.addOrUpdateEntry(
1193@@ -34,7 +35,7 @@
1194
1195 The details include the project the entry is for, and who uploaded it.
1196
1197- >>> product.displayname in details_text
1198+ >>> product_displayname in details_text
1199 True
1200
1201 # Must remove the security proxy because IPerson.displayname is protected.
1202
1203=== modified file 'lib/lp/translations/stories/webservice/xx-potemplate.txt'
1204--- lib/lp/translations/stories/webservice/xx-potemplate.txt 2011-05-27 19:53:20 +0000
1205+++ lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-04 10:27:37 +0000
1206@@ -71,13 +71,14 @@
1207
1208 >>> login('admin@canonical.com')
1209 >>> productseries = factory.makeProductSeries()
1210+ >>> product_name = productseries.product.name
1211 >>> potemplate_1 = factory.makePOTemplate(productseries=productseries)
1212 >>> potemplate_2 = factory.makePOTemplate(productseries=productseries)
1213 >>> potemplate_count = 2
1214 >>> logout()
1215 >>> all_translation_templates = anon_webservice.named_get(
1216 ... '/%s/%s' % (
1217- ... productseries.product.name,
1218+ ... product_name,
1219 ... productseries.name),
1220 ... 'getTranslationTemplates'
1221 ... ).jsonBody()