Merge lp:~adeuring/launchpad/product-sharing-sec-adapter into lp:launchpad

Proposed by Abel Deuring on 2012-10-02
Status: Merged
Approved by: Abel Deuring on 2012-10-02
Approved revision: no longer in the source branch.
Merged at revision: 16090
Proposed branch: lp:~adeuring/launchpad/product-sharing-sec-adapter
Merge into: lp:launchpad
Diff against target: 688 lines (+373/-53)
8 files modified
lib/lp/registry/configure.zcml (+24/-12)
lib/lp/registry/interfaces/product.py (+23/-17)
lib/lp/registry/model/product.py (+18/-9)
lib/lp/registry/tests/test_product.py (+254/-0)
lib/lp/registry/tests/test_product_webservice.py (+22/-13)
lib/lp/security.py (+25/-0)
lib/lp/testing/factory.py (+4/-1)
lib/lp/testing/pages.py (+3/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/product-sharing-sec-adapter
Reviewer Review Type Date Requested Status
Francesco Banconi (community) 2012-10-02 Approve on 2012-10-02
Review via email: mp+127473@code.launchpad.net

Commit Message

require the permission launchpad.View for formerly public properties of IProduct; replace the permission launchpad.AnyAllowedPerson instead of launchpad.AnyPerson

Description of the Change

This branch changes the security configuration for IProduct.

Access to most properties of IProduct now requires the permission
launchpad.LimitedView or launchpad.AnyAllowedPerson. The only still
publicly available properties are id, information_type and a new method
userCanView()

Details:

- A new interface class IProductLimitedView, which defines most
  properties that were previously defined in IProductPublic

- IProductPublic now defines only the properties id, information_type
  and userCanView()

- registry/configure.zcml now requires the permissions launchpad.LimitedView
  or launchpad.AnyAllowedPerson for most "partial interfaces" of IProduct.

- new security adapters ViewProduct (for the permission lp.LimitedView)
  and ChangeProduct (for the permission lp.AnyAllowedPerson).
  These security adapters check if a project is public; if so, they allow
  access for any logged in person; ViewProduct also allws access for
  anonymous users in this case. For non-public projects, these adapters
  use the new method IProduct.userCanView() to check if the current user
  may be given access.

- the implementation of userCanView() is obviously incomplete: Only
  a project owner can currently access data of a private project.
  I will change this in a follow-up branch so that userCanView() will
  check if the user has an access policy grant for the given product.
  (The implementation of this change would have made the diff of this
  branch a bit too large ;)

  The implementation of IProduct.information_type is also still
  incomplete (the DB patch that adds a column Product.information_type
  has not yet been applied). Products are by default public; only some
  new tests set information_type to a non-public state, but this
  change is obviously not permanent. Hence there on risk that properties
  of any existing product become accidentally inaccessible.

- the tests test_get_permissions() and test_set_permissions() may look a
  bit excessive, but having the permissions neede for all properties
  documented in the dictionaries expected_[gs]et_permissions helped
  me to check that I did not leave any property publicly accessible.

- I had to change some websevice tests: webservice_for_person() calls
  endInteraction(), so we don't have any user/principal defined when
  this function finishes. This mean that access to an attribute of a
  product will fail, if no new interaction is created. This is why the
  changes store for example product.owner in alocal variable right after
  the product has been generated. For assertions that access
  product attributes, I used the context manager person_logged_in().

tests:

bin/test -vvt lp.registry.tests.test_product.TestProduct.test_.et_permissions
bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_LimitedView_proprietary_product
bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_LimitedView_public_product
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_AnyAllowedPerson_public_product

no lint.

To post a comment you must log in.
Francesco Banconi (frankban) wrote :

Looks good Abel, thank you. Some minor details follow.

256 + def check_permissions(self, expected_permissions, used_permissions,
257 + type_):

There is a check_permissions function in lp.blueprints.tests.test_specification, which is very similar. Maybe we could abstract them out.

420 + def test_access_launchpad_View_proprietary_product(self):
421 + # Only people with grants for a prviate product can access
422 + # attributes protected by the permission launchapd.View.

Typos: private, launchpad. The same below in test_access_launchpad_AnyAllowedPerson_proprietary_product and in
test_set_launchpad_AnyAllowedPerson_proprietary_product.

439 + def test_access_launchpad_AnyAllowedPerson_public_product(self):
440 + # Only logged in persons hav access to properties of public products

Typo: have

475 + def test_set_launchpad_AnyAllowedPerson_public_product(self):
476 + # Only logged in users can set attributes protected by the
477 + # permission launchapd.AnyAllowedPerson.

Typo: launchpad

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/configure.zcml'
2--- lib/lp/registry/configure.zcml 2012-09-25 04:29:34 +0000
3+++ lib/lp/registry/configure.zcml 2012-10-02 14:25:30 +0000
4@@ -1229,11 +1229,17 @@
5 class="lp.registry.model.product.Product">
6 <allow
7 interface="lp.registry.interfaces.product.IProductPublic"/>
8- <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
9- <allow
10+ <require
11+ permission="launchpad.View"
12+ interface="lp.registry.interfaces.product.IProductLimitedView"/>
13+ <require
14+ permission="launchpad.View"
15+ interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
16+ <require
17+ permission="launchpad.View"
18 interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
19 <require
20- permission="launchpad.View"
21+ permission="launchpad.AnyAllowedPerson"
22 set_attributes="date_next_suggest_packaging"/>
23 <require
24 permission="launchpad.Driver"
25@@ -1311,7 +1317,7 @@
26 translationpermission
27 translations_usage"/>
28 <require
29- permission="zope.Public"
30+ permission="launchpad.View"
31 attributes="
32 qualifies_for_free_hosting"/>
33 <require
34@@ -1326,7 +1332,8 @@
35
36 <!-- IHasAliases -->
37
38- <allow
39+ <require
40+ permission="launchpad.View"
41 attributes="
42 aliases"/>
43 <require
44@@ -1336,14 +1343,17 @@
45
46 <!-- IQuestionTarget -->
47
48- <allow interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
49- <require
50- permission="launchpad.AnyPerson"
51+ <require
52+ permission="launchpad.View"
53+ interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
54+ <require
55+ permission="launchpad.AnyAllowedPerson"
56 interface="lp.answers.interfaces.questiontarget.IQuestionTargetView"/>
57
58 <!-- IFAQTarget -->
59
60- <allow
61+ <require
62+ permission="launchpad.View"
63 interface="lp.answers.interfaces.faqcollection.IFAQCollection"
64 attributes="
65 findSimilarFAQs"/>
66@@ -1354,15 +1364,17 @@
67
68 <!-- IStructuralSubscriptionTarget -->
69
70- <allow
71+ <require
72+ permission="launchpad.View"
73 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
74 <require
75- permission="launchpad.AnyPerson"
76+ permission="launchpad.AnyAllowedPerson"
77 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
78
79 <!-- IHasBugSupervisor -->
80
81- <allow
82+ <require
83+ permission="launchpad.View"
84 attributes="
85 bug_supervisor"/>
86 <require
87
88=== modified file 'lib/lp/registry/interfaces/product.py'
89--- lib/lp/registry/interfaces/product.py 2012-09-28 06:15:58 +0000
90+++ lib/lp/registry/interfaces/product.py 2012-10-02 14:25:30 +0000
91@@ -422,7 +422,22 @@
92 "Not applicable to 'Other/Proprietary'.")))
93
94
95-class IProductPublic(
96+class IProductPublic(Interface):
97+
98+ id = Int(title=_('The Project ID'))
99+
100+ information_type = exported(
101+ Choice(
102+ title=_('Information Type'), vocabulary=InformationType,
103+ required=True, readonly=True,
104+ description=_(
105+ 'The type of of data contained in this project.')))
106+
107+ def userCanView(user):
108+ """True if the given user has access to this product."""
109+
110+
111+class IProductLimitedView(
112 IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
113 IHasBranchVisibilityPolicy, IHasDrivers, IHasExternalBugTracker, IHasIcon,
114 IHasLogo, IHasMergeProposals, IHasMilestones,
115@@ -432,8 +447,6 @@
116 ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):
117 """Public IProduct properties."""
118
119- id = Int(title=_('The Project ID'))
120-
121 project = exported(
122 ReferenceChoice(
123 title=_('Part of'),
124@@ -450,13 +463,6 @@
125 'and security policy will apply to this project.')),
126 exported_as='project_group')
127
128- information_type = exported(
129- Choice(
130- title=_('Information Type'), vocabulary=InformationType,
131- required=True, readonly=True,
132- description=_(
133- 'The type of of data contained in this project.')))
134-
135 owner = exported(
136 PersonChoice(
137 title=_('Maintainer'),
138@@ -893,9 +899,9 @@
139 class IProductEditRestricted(IOfficialBugTagTargetRestricted):
140 """`IProduct` properties which require launchpad.Edit permission."""
141
142- @mutator_for(IProductPublic['bug_sharing_policy'])
143+ @mutator_for(IProductLimitedView['bug_sharing_policy'])
144 @operation_parameters(bug_sharing_policy=copy_field(
145- IProductPublic['bug_sharing_policy']))
146+ IProductLimitedView['bug_sharing_policy']))
147 @export_write_operation()
148 @operation_for_version("devel")
149 def setBugSharingPolicy(bug_sharing_policy):
150@@ -904,10 +910,10 @@
151 Checks authorization and entitlement.
152 """
153
154- @mutator_for(IProductPublic['branch_sharing_policy'])
155+ @mutator_for(IProductLimitedView['branch_sharing_policy'])
156 @operation_parameters(
157 branch_sharing_policy=copy_field(
158- IProductPublic['branch_sharing_policy']))
159+ IProductLimitedView['branch_sharing_policy']))
160 @export_write_operation()
161 @operation_for_version("devel")
162 def setBranchSharingPolicy(branch_sharing_policy):
163@@ -916,10 +922,10 @@
164 Checks authorization and entitlement.
165 """
166
167- @mutator_for(IProductPublic['specification_sharing_policy'])
168+ @mutator_for(IProductLimitedView['specification_sharing_policy'])
169 @operation_parameters(
170 specification_sharing_policy=copy_field(
171- IProductPublic['specification_sharing_policy']))
172+ IProductLimitedView['specification_sharing_policy']))
173 @export_write_operation()
174 @operation_for_version("devel")
175 def setSpecificationSharingPolicy(specification_sharing_policy):
176@@ -932,7 +938,7 @@
177 class IProduct(
178 IHasBugSupervisor, IProductEditRestricted,
179 IProductModerateRestricted, IProductDriverRestricted,
180- IProductPublic, IQuestionTarget, IRootContext,
181+ IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
182 IStructuralSubscriptionTarget):
183 """A Product.
184
185
186=== modified file 'lib/lp/registry/model/product.py'
187--- lib/lp/registry/model/product.py 2012-09-28 19:59:35 +0000
188+++ lib/lp/registry/model/product.py 2012-10-02 14:25:30 +0000
189@@ -69,6 +69,7 @@
190 FREE_INFORMATION_TYPES,
191 InformationType,
192 PRIVATE_INFORMATION_TYPES,
193+ PUBLIC_INFORMATION_TYPES,
194 service_uses_launchpad,
195 ServiceUsage,
196 )
197@@ -413,15 +414,17 @@
198 """
199 pass
200
201- @property
202- def information_type(self):
203- """See `IProduct`
204-
205- Place holder for a db column.
206- XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
207- into place.
208- """
209- pass
210+ # Place holder for a db column.
211+ # XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
212+ # into place.
213+ def _get_information_type(self):
214+ return getattr(
215+ self, '_information_type', InformationType.PUBLIC)
216+
217+ def _set_information_type(self, value):
218+ self._information_type = value
219+
220+ information_type = property(_get_information_type, _set_information_type)
221
222 security_contact = None
223
224@@ -1518,6 +1521,12 @@
225
226 return weight_function
227
228+ def userCanView(self, user):
229+ """See `IProductPublic`."""
230+ if self.information_type in PUBLIC_INFORMATION_TYPES:
231+ return True
232+ return user.inTeam(self.owner)
233+
234
235 class ProductSet:
236 implements(IProductSet)
237
238=== modified file 'lib/lp/registry/tests/test_product.py'
239--- lib/lp/registry/tests/test_product.py 2012-09-28 06:15:58 +0000
240+++ lib/lp/registry/tests/test_product.py 2012-10-02 14:25:30 +0000
241@@ -11,6 +11,10 @@
242 import transaction
243 from zope.component import getUtility
244 from zope.lifecycleevent.interfaces import IObjectModifiedEvent
245+from zope.security.checker import (
246+ CheckerPublic,
247+ getChecker,
248+ )
249 from zope.security.interfaces import Unauthorized
250 from zope.security.proxy import removeSecurityProxy
251
252@@ -390,6 +394,256 @@
253 expected = [InformationType.PROPRIETARY]
254 self.assertContentEqual(expected, [policy.type for policy in aps])
255
256+ def check_permissions(self, expected_permissions, used_permissions,
257+ type_):
258+ expected = set(expected_permissions.keys())
259+ self.assertEqual(
260+ expected, set(used_permissions.values()),
261+ 'Unexpected %s permissions' % type_)
262+ for permission in expected_permissions:
263+ attribute_names = set(
264+ name for name, value in used_permissions.items()
265+ if value == permission)
266+ self.assertEqual(
267+ expected_permissions[permission], attribute_names,
268+ 'Unexpected set of attributes with %s permission %s:\n'
269+ 'Defined but not expected: %s\n'
270+ 'Expected but not defined: %s'
271+ % (
272+ type_, permission,
273+ sorted(
274+ attribute_names - expected_permissions[permission]),
275+ sorted(
276+ expected_permissions[permission] - attribute_names)))
277+
278+ expected_get_permissions = {
279+ CheckerPublic: set(('id', 'information_type', 'userCanView',)),
280+ 'launchpad.View': set((
281+ '_getOfficialTagClause', 'active', 'active_or_packaged_series',
282+ 'aliases', 'all_milestones', 'all_specifications',
283+ 'allowsTranslationEdits', 'allowsTranslationSuggestions',
284+ 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
285+ 'blueprints_usage', 'branch_sharing_policy',
286+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
287+ 'bug_sharing_policy', 'bug_subscriptions', 'bug_supervisor',
288+ 'bug_tracking_usage', 'bugtargetdisplayname', 'bugtargetname',
289+ 'bugtracker', 'canUserAlterAnswerContact',
290+ 'checkPrivateBugsTransitionAllowed', 'codehosting_usage',
291+ 'coming_sprints', 'commercial_subscription',
292+ 'commercial_subscription_is_due', 'createBug',
293+ 'createCustomLanguageCode', 'custom_language_codes',
294+ 'date_next_suggest_packaging', 'datecreated', 'description',
295+ 'development_focus', 'development_focusID',
296+ 'direct_answer_contacts', 'displayname', 'distrosourcepackages',
297+ 'downloadurl', 'driver', 'drivers', 'enable_bug_expiration',
298+ 'enable_bugfiling_duplicate_search', 'findReferencedOOPS',
299+ 'findSimilarFAQs', 'findSimilarQuestions', 'freshmeatproject',
300+ 'getAllowedBugInformationTypes',
301+ 'getAllowedSpecificationInformationTypes', 'getAnnouncement',
302+ 'getAnnouncements', 'getAnswerContactsForLanguage',
303+ 'getAnswerContactRecipients', 'getBaseBranchVisibilityRule',
304+ 'getBranchVisibilityRuleForBranch',
305+ 'getBranchVisibilityRuleForTeam',
306+ 'getBranchVisibilityTeamPolicies', 'getBranches',
307+ 'getBugSummaryContextWhereClause', 'getBugTaskWeightFunction',
308+ 'getCustomLanguageCode', 'getDefaultBugInformationType',
309+ 'getDefaultSpecificationInformationType',
310+ 'getEffectiveTranslationPermission', 'getExternalBugTracker',
311+ 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
312+ 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
313+ 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
314+ 'getSeries', 'getSpecification', 'getSubscription',
315+ 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
316+ 'getTopContributors', 'getTopContributorsGroupedByCategory',
317+ 'getTranslationGroups', 'getTranslationImportQueueEntries',
318+ 'getTranslators', 'getUsedBugTagsWithOpenCounts',
319+ 'getVersionSortedSeries', 'has_any_specifications',
320+ 'has_current_commercial_subscription',
321+ 'has_custom_language_codes', 'has_milestones', 'homepage_content',
322+ 'homepageurl', 'icon', 'invitesTranslationEdits',
323+ 'invitesTranslationSuggestions',
324+ 'isUsingInheritedBranchVisibilityPolicy',
325+ 'latest_completed_specifications', 'latest_specifications',
326+ 'license_info', 'license_status', 'licenses', 'logo', 'milestones',
327+ 'mugshot', 'name', 'name_with_project', 'newCodeImport',
328+ 'obsolete_translatable_series', 'official_answers',
329+ 'official_anything', 'official_blueprints', 'official_bug_tags',
330+ 'official_codehosting', 'official_malone', 'owner',
331+ 'parent_subscription_target', 'packagedInDistros', 'packagings',
332+ 'past_sprints', 'personHasDriverRights', 'pillar',
333+ 'pillar_category', 'primary_translatable', 'private_bugs',
334+ 'programminglang', 'project', 'qualifies_for_free_hosting',
335+ 'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
336+ 'remote_product', 'removeCustomLanguageCode',
337+ 'removeTeamFromBranchVisibilityPolicy', 'screenshotsurl',
338+ 'searchFAQs', 'searchQuestions', 'searchTasks', 'security_contact',
339+ 'series', 'setBranchVisibilityTeamPolicy', 'setPrivateBugs',
340+ 'sharesTranslationsWithOtherSide', 'sourceforgeproject',
341+ 'sourcepackages', 'specification_sharing_policy', 'specifications',
342+ 'sprints', 'summary', 'target_type_display', 'title',
343+ 'translatable_packages', 'translatable_series',
344+ 'translation_focus', 'translationgroup', 'translationgroups',
345+ 'translationpermission', 'translations_usage', 'ubuntu_packages',
346+ 'userCanAlterBugSubscription', 'userCanAlterSubscription',
347+ 'userCanEdit', 'userHasBugSubscriptions', 'uses_launchpad',
348+ 'valid_specifications', 'wikiurl')),
349+ 'launchpad.AnyAllowedPerson': set((
350+ 'addAnswerContact', 'addBugSubscription',
351+ 'addBugSubscriptionFilter', 'addSubscription',
352+ 'createQuestionFromBug', 'newQuestion', 'removeAnswerContact',
353+ 'removeBugSubscription')),
354+ 'launchpad.Append': set(('newFAQ', )),
355+ 'launchpad.Driver': set(('newSeries', )),
356+ 'launchpad.Edit': set((
357+ 'addOfficialBugTag', 'removeOfficialBugTag',
358+ 'setBranchSharingPolicy', 'setBugSharingPolicy',
359+ 'setSpecificationSharingPolicy')),
360+ 'launchpad.Moderate': set((
361+ 'is_permitted', 'license_approved', 'project_reviewed',
362+ 'reviewer_whiteboard', 'setAliases')),
363+ }
364+
365+ def test_get_permissions(self):
366+ product = self.factory.makeProduct()
367+ checker = getChecker(product)
368+ self.check_permissions(
369+ self.expected_get_permissions, checker.get_permissions, 'get')
370+
371+ def test_set_permissions(self):
372+ expected_set_permissions = {
373+ 'launchpad.BugSupervisor': set((
374+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
375+ 'bugtracker', 'enable_bug_expiration',
376+ 'enable_bugfiling_duplicate_search', 'official_bug_tags',
377+ 'official_malone', 'remote_product')),
378+ 'launchpad.Edit': set((
379+ 'answers_usage', 'blueprints_usage', 'bug_supervisor',
380+ 'bug_tracking_usage', 'codehosting_usage',
381+ 'commercial_subscription', 'description', 'development_focus',
382+ 'displayname', 'downloadurl', 'driver', 'freshmeatproject',
383+ 'homepage_content', 'homepageurl', 'icon', 'license_info',
384+ 'licenses', 'logo', 'mugshot', 'official_answers',
385+ 'official_blueprints', 'official_codehosting', 'owner',
386+ 'programminglang', 'project', 'redeemSubscriptionVoucher',
387+ 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
388+ 'summary', 'title', 'uses_launchpad', 'wikiurl')),
389+ 'launchpad.Moderate': set((
390+ 'active', 'autoupdate', 'license_approved', 'name',
391+ 'project_reviewed', 'registrant', 'reviewer_whiteboard')),
392+ 'launchpad.TranslationsAdmin': set((
393+ 'translation_focus', 'translationgroup',
394+ 'translationpermission', 'translations_usage')),
395+ 'launchpad.AnyAllowedPerson': set((
396+ 'date_next_suggest_packaging', )),
397+ }
398+ product = self.factory.makeProduct()
399+ checker = getChecker(product)
400+ self.check_permissions(
401+ expected_set_permissions, checker.set_permissions, 'set')
402+
403+ def test_access_launchpad_View_public_product(self):
404+ # Everybody, including anonymous users, has access to
405+ # properties of public products that require the permission
406+ # launchpad.View
407+ product = self.factory.makeProduct()
408+ names = self.expected_get_permissions['launchpad.View']
409+ with person_logged_in(None):
410+ for attribute_name in names:
411+ getattr(product, attribute_name)
412+ ordinary_user = self.factory.makePerson()
413+ with person_logged_in(ordinary_user):
414+ for attribute_name in names:
415+ getattr(product, attribute_name)
416+ with person_logged_in(product.owner):
417+ for attribute_name in names:
418+ getattr(product, attribute_name)
419+
420+ def test_access_launchpad_View_proprietary_product(self):
421+ # Only people with grants for a private product can access
422+ # attributes protected by the permission launchpad.View.
423+ product = self.factory.makeProduct(
424+ information_type=InformationType.PROPRIETARY)
425+ names = self.expected_get_permissions['launchpad.View']
426+ with person_logged_in(None):
427+ for attribute_name in names:
428+ self.assertRaises(
429+ Unauthorized, getattr, product, attribute_name)
430+ ordinary_user = self.factory.makePerson()
431+ with person_logged_in(ordinary_user):
432+ for attribute_name in names:
433+ self.assertRaises(
434+ Unauthorized, getattr, product, attribute_name)
435+ with person_logged_in(removeSecurityProxy(product).owner):
436+ for attribute_name in names:
437+ getattr(product, attribute_name)
438+
439+ def test_access_launchpad_AnyAllowedPerson_public_product(self):
440+ # Only logged in persons have access to properties of public products
441+ # that require the permission launchpad.AnyAllowedPerson.
442+ product = self.factory.makeProduct()
443+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
444+ with person_logged_in(None):
445+ for attribute_name in names:
446+ self.assertRaises(
447+ Unauthorized, getattr, product, attribute_name)
448+ ordinary_user = self.factory.makePerson()
449+ with person_logged_in(ordinary_user):
450+ for attribute_name in names:
451+ getattr(product, attribute_name)
452+ with person_logged_in(product.owner):
453+ for attribute_name in names:
454+ getattr(product, attribute_name)
455+
456+ def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
457+ # Only people with grants for a private product can access
458+ # attributes protected by the permission launchpad.AnyAllowedPerson.
459+ product = self.factory.makeProduct(
460+ information_type=InformationType.PROPRIETARY)
461+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
462+ with person_logged_in(None):
463+ for attribute_name in names:
464+ self.assertRaises(
465+ Unauthorized, getattr, product, attribute_name)
466+ ordinary_user = self.factory.makePerson()
467+ with person_logged_in(ordinary_user):
468+ for attribute_name in names:
469+ self.assertRaises(
470+ Unauthorized, getattr, product, attribute_name)
471+ with person_logged_in(removeSecurityProxy(product).owner):
472+ for attribute_name in names:
473+ getattr(product, attribute_name)
474+
475+ def test_set_launchpad_AnyAllowedPerson_public_product(self):
476+ # Only logged in users can set attributes protected by the
477+ # permission launchpad.AnyAllowedPerson.
478+ product = self.factory.makeProduct()
479+ with person_logged_in(None):
480+ self.assertRaises(
481+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
482+ 'foo')
483+ ordinary_user = self.factory.makePerson()
484+ with person_logged_in(ordinary_user):
485+ setattr(product, 'date_next_suggest_packaging', 'foo')
486+ with person_logged_in(product.owner):
487+ setattr(product, 'date_next_suggest_packaging', 'foo')
488+
489+ def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
490+ # Only people with grants for a private product can set
491+ # attributes protected by the permission launchpad.AnyAllowedPerson.
492+ product = self.factory.makeProduct(
493+ information_type=InformationType.PROPRIETARY)
494+ with person_logged_in(None):
495+ self.assertRaises(
496+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
497+ 'foo')
498+ ordinary_user = self.factory.makePerson()
499+ with person_logged_in(ordinary_user):
500+ self.assertRaises(
501+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
502+ 'foo')
503+ with person_logged_in(removeSecurityProxy(product).owner):
504+ setattr(product, 'date_next_suggest_packaging', 'foo')
505+
506
507 class TestProductBugInformationTypes(TestCaseWithFactory):
508
509
510=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
511--- lib/lp/registry/tests/test_product_webservice.py 2012-09-19 05:15:39 +0000
512+++ lib/lp/registry/tests/test_product_webservice.py 2012-10-02 14:25:30 +0000
513@@ -17,6 +17,7 @@
514 from lp.services.webapp.publisher import canonical_url
515 from lp.testing import TestCaseWithFactory
516 from lp.testing.layers import DatabaseFunctionalLayer
517+from lp.testing import person_logged_in
518 from lp.testing.pages import (
519 LaunchpadWebServiceCaller,
520 webservice_for_person,
521@@ -47,27 +48,30 @@
522 layer = DatabaseFunctionalLayer
523
524 def patch(self, webservice, obj, **data):
525+ with person_logged_in(webservice.user):
526+ path = URI(canonical_url(obj)).path
527 return webservice.patch(
528- URI(canonical_url(obj)).path,
529- 'application/json', json.dumps(data),
530- api_version='devel')
531+ path, 'application/json', json.dumps(data), api_version='devel')
532
533 def test_branch_sharing_policy_can_be_set(self):
534 # branch_sharing_policy can be set via the API.
535 product = self.factory.makeProduct()
536+ owner = product.owner
537 self.factory.makeCommercialSubscription(product=product)
538 webservice = webservice_for_person(
539- product.owner, permission=OAuthPermission.WRITE_PRIVATE)
540+ owner, permission=OAuthPermission.WRITE_PRIVATE)
541 response = self.patch(
542 webservice, product, branch_sharing_policy='Proprietary')
543 self.assertEqual(209, response.status)
544- self.assertEqual(
545- BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
546+ with person_logged_in(owner):
547+ self.assertEqual(
548+ BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
549
550 def test_branch_sharing_policy_non_commercial(self):
551 # An API attempt to set a commercial-only branch_sharing_policy
552 # on a non-commercial project returns Forbidden.
553 product = self.factory.makeLegacyProduct()
554+ owner = product.owner
555 webservice = webservice_for_person(
556 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
557 response = self.patch(
558@@ -76,24 +80,28 @@
559 status=403,
560 body=('A current commercial subscription is required to use '
561 'proprietary branches.')))
562- self.assertIs(None, product.branch_sharing_policy)
563+ with person_logged_in(owner):
564+ self.assertIs(None, product.branch_sharing_policy)
565
566 def test_bug_sharing_policy_can_be_set(self):
567 # bug_sharing_policy can be set via the API.
568 product = self.factory.makeProduct()
569+ owner = product.owner
570 self.factory.makeCommercialSubscription(product=product)
571 webservice = webservice_for_person(
572 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
573 response = self.patch(
574 webservice, product, bug_sharing_policy='Proprietary')
575 self.assertEqual(209, response.status)
576- self.assertEqual(
577- BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
578+ with person_logged_in(owner):
579+ self.assertEqual(
580+ BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
581
582 def test_bug_sharing_policy_non_commercial(self):
583 # An API attempt to set a commercial-only bug_sharing_policy
584 # on a non-commercial project returns Forbidden.
585 product = self.factory.makeLegacyProduct()
586+ owner = product.owner
587 webservice = webservice_for_person(
588 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
589 response = self.patch(
590@@ -102,12 +110,13 @@
591 status=403,
592 body=('A current commercial subscription is required to use '
593 'proprietary bugs.')))
594- self.assertIs(None, product.bug_sharing_policy)
595+ with person_logged_in(owner):
596+ self.assertIs(None, product.bug_sharing_policy)
597
598 def fetch_product(self, webservice, product, api_version):
599- return webservice.get(
600- canonical_url(product, force_local_path=True),
601- api_version=api_version).jsonBody()
602+ with person_logged_in(webservice.user):
603+ url = canonical_url(product, force_local_path=True)
604+ return webservice.get(url, api_version=api_version).jsonBody()
605
606 def test_security_contact_exported(self):
607 # security_contact is exported for 1.0, but not for other versions.
608
609=== modified file 'lib/lp/security.py'
610--- lib/lp/security.py 2012-09-19 04:09:06 +0000
611+++ lib/lp/security.py 2012-10-02 14:25:30 +0000
612@@ -34,6 +34,7 @@
613 from lp.answers.interfaces.questionmessage import IQuestionMessage
614 from lp.answers.interfaces.questionsperson import IQuestionsPerson
615 from lp.answers.interfaces.questiontarget import IQuestionTarget
616+from lp.app.enums import PUBLIC_INFORMATION_TYPES
617 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
618 from lp.app.interfaces.security import IAuthorization
619 from lp.app.security import (
620@@ -430,6 +431,30 @@
621 return user.isOwner(self.obj) or user.in_admin
622
623
624+class ViewProduct(AuthorizationBase):
625+ permission = 'launchpad.View'
626+ usedfor = IProduct
627+
628+ def checkAuthenticated(self, user):
629+ return self.obj.userCanView(user)
630+
631+ def checkUnauthenticated(self):
632+ return self.obj.information_type in PUBLIC_INFORMATION_TYPES
633+
634+
635+class ChangeProduct(ViewProduct):
636+ """Used for attributes of IProduct that are accessible to any logged
637+ in user for public product but only to persons with access grants
638+ for private products.
639+ """
640+
641+ permission = 'launchpad.AnyAllowedPerson'
642+ usedfor = IProduct
643+
644+ def checkUnauthenticated(self):
645+ return False
646+
647+
648 class EditProduct(EditByOwnersOrAdmins):
649 usedfor = IProduct
650
651
652=== modified file 'lib/lp/testing/factory.py'
653--- lib/lp/testing/factory.py 2012-10-02 07:08:13 +0000
654+++ lib/lp/testing/factory.py 2012-10-02 14:25:30 +0000
655@@ -964,7 +964,8 @@
656 title=None, summary=None, official_malone=None,
657 translations_usage=None, bug_supervisor=None, private_bugs=False,
658 driver=None, icon=None, bug_sharing_policy=None,
659- branch_sharing_policy=None, specification_sharing_policy=None):
660+ branch_sharing_policy=None, specification_sharing_policy=None,
661+ information_type=None):
662 """Create and return a new, arbitrary Product."""
663 if owner is None:
664 owner = self.makePerson()
665@@ -1020,6 +1021,8 @@
666 if specification_sharing_policy:
667 naked_product.setSpecificationSharingPolicy(
668 specification_sharing_policy)
669+ if information_type is not None:
670+ naked_product.information_type = information_type
671
672 return product
673
674
675=== modified file 'lib/lp/testing/pages.py'
676--- lib/lp/testing/pages.py 2012-08-14 23:27:07 +0000
677+++ lib/lp/testing/pages.py 2012-10-02 14:25:30 +0000
678@@ -730,7 +730,9 @@
679 request_token.review(person, permission, context)
680 access_token = request_token.createAccessToken()
681 logout()
682- return LaunchpadWebServiceCaller(consumer_key, access_token.key)
683+ service = LaunchpadWebServiceCaller(consumer_key, access_token.key)
684+ service.user = person
685+ return service
686
687
688 def setupDTCBrowser():