Merge lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad

Proposed by Abel Deuring on 2012-10-16
Status: Merged
Approved by: Abel Deuring on 2012-10-17
Approved revision: no longer in the source branch.
Merged at revision: 16158
Proposed branch: lp:~adeuring/launchpad/milestone-sec-adapter
Merge into: lp:launchpad
Diff against target: 601 lines (+356/-77)
9 files modified
lib/lp/blueprints/tests/test_specification.py (+2/-22)
lib/lp/registry/configure.zcml (+28/-21)
lib/lp/registry/interfaces/milestone.py (+3/-0)
lib/lp/registry/model/milestone.py (+11/-0)
lib/lp/registry/tests/test_milestone.py (+247/-0)
lib/lp/registry/tests/test_person.py (+13/-9)
lib/lp/registry/tests/test_product.py (+2/-24)
lib/lp/security.py (+20/-1)
lib/lp/testing/__init__.py (+30/-0)
To merge this branch: bzr merge lp:~adeuring/launchpad/milestone-sec-adapter
Reviewer Review Type Date Requested Status
Richard Harding (community) 2012-10-16 Approve on 2012-10-16
Review via email: mp+129917@code.launchpad.net

Commit Message

require policy grants for access to milestone related to non-public prudcts.

Description of the Change

This branch adds a "sharing wawre" security adapter for IMilestone.

Milestones related to private products are now only shown to persons
with policy grants for the product; the only exception are some
attributes that leak no information: the database ID, and the methods
userCanView(), checkAuthenticated(), checkUnauthenticated().

The two latter methods should not appear at all -- we use adapters
for the security checks, so there is no need to define them in the
main class. And in fact they are not implemented at all.

Similary, IMilestone and some other interface classes included in
IMilestone define a few attributes that not implemented by class
Milestone.

We do not have very much time to finish the "Private products" story,
so I simply tried to ignore these inconsistencies.

Details of the change:

lp/registry/configure.zcml:

Access to most attributes and to "partial" interfaces that were public
now requires the permission launchpad.View; the permission
launchpad.AnyPerson is replaced with launchapd.AnyAllowedPerson.

(lp.services.webapp.authorization.LaunchpadSecurityPolicy.checkPermission()
has a "shortcut" for the permission launchpad.AnyPerson: no
dedicated security adapters are looked up for this permission,
so the new rule "data for milestones of private product should only
be visible for person having a policy grant" cannot be implemented
with this permission.)

I also sorted the attributes requiring lp.View alphabetically.

lp/security.py:

two new adapters for IMilestone and the permissions lp.View and
lp.AnyAllowedPerson, respectively.

Both methods delegate the security check to the new method
Milestone.userCanView()

lp/registry/model/milestone.py:

The new method userCanView(). The acutal permission check is done
by IProduct.userCanView()

lp/registry/tests/test_milestone.py:

Tests for the permissions.

The test class properties expected_get_permissions and
expected_set_permissions are also intended to document which permissions
are acutally used for IMilestone.

The tests are similar to the test pattern I also used for the
security adapters of IProduct and ISpecification, so I moved the
common method checkPermissions() to lp.testing.TestCase.

test:
./bin/test registry -vvt lp.registry.tests.test_milestone.MilestoneSecurityAdaperTestCase

no lint

To post a comment you must log in.
Richard Harding (rharding) wrote :

Thanks Abel, looks good with some typo nitpicks and one question for you.

#155 Should the check here be against distributions directed instead of indirectly using product?

#260 Typo in assertAccessAuthorzized (Authorized) [ok, find and repeat throughout here]

#264 Typo implenet (implement)

#285 "may not"

#302 "have access to public"

#328 I would have expected the user to have access to the information on a public milestone? Is this an incorrect assumption? I guess not since they'd not have access to launchpad.View permissions.

review: Approve
Abel Deuring (adeuring) wrote :

Rick, thanks for the review.

Regarding #155:

153 + def userCanView(self, user):
154 + """See `IMilestone`."""
155 + # A database constraint ensures that either self.product
156 + # or self.distribution is not None.
157 + if self.product is None:
158 + # Distributions are always public, and so are their
159 + # milestones.
160 + return True

I think it does not matter effectively if we check against self.product is None or self.distribution is None because the DB constraint ensures that exactly one of self.distribution and self.product is None.

#328 is about arrtibutes that require other permissions that CHeckPublic or lp.View, which means the permissions lp.AnyPerson and lp.Edit. (Note the "continue" in line 327)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/tests/test_specification.py'
2--- lib/lp/blueprints/tests/test_specification.py 2012-09-24 13:43:00 +0000
3+++ lib/lp/blueprints/tests/test_specification.py 2012-10-17 15:25:27 +0000
4@@ -139,26 +139,6 @@
5 self.assertRaises(
6 Unauthorized, getattr, specification, 'setTarget')
7
8- def check_permissions(self, expected_permissions, used_permissions,
9- type_):
10- expected = set(expected_permissions.keys())
11- self.assertEqual(
12- expected, set(used_permissions.values()),
13- 'Unexpected %s permissions' % type_)
14- for permission in expected_permissions:
15- attribute_names = set(
16- name for name, value in used_permissions.items()
17- if value == permission)
18- self.assertEqual(
19- expected_permissions[permission], attribute_names,
20- 'Unexpected set of attributes with %s permission %s:\n'
21- 'Defined but not expected: %s\n'
22- 'Expected but not defined: %s'
23- % (
24- type_, permission,
25- attribute_names - expected_permissions[permission],
26- expected_permissions[permission] - attribute_names))
27-
28 def test_get_permissions(self):
29 expected_get_permissions = {
30 CheckerPublic: set((
31@@ -196,7 +176,7 @@
32 }
33 specification = self.factory.makeSpecification()
34 checker = getChecker(specification)
35- self.check_permissions(
36+ self.checkPermissions(
37 expected_get_permissions, checker.get_permissions, 'get')
38
39 def test_set_permissions(self):
40@@ -211,7 +191,7 @@
41 }
42 specification = self.factory.makeSpecification()
43 checker = getChecker(specification)
44- self.check_permissions(
45+ self.checkPermissions(
46 expected_get_permissions, checker.set_permissions, 'set')
47
48 def test_security_adapters(self):
49
50=== modified file 'lib/lp/registry/configure.zcml'
51--- lib/lp/registry/configure.zcml 2012-10-09 10:28:02 +0000
52+++ lib/lp/registry/configure.zcml 2012-10-17 15:25:27 +0000
53@@ -1026,22 +1026,30 @@
54 <allow
55 attributes="
56 id
57- name
58+ userCanView
59+ " />
60+ <require
61+ permission="launchpad.View"
62+ attributes="
63+ active
64+ bugtasks
65 code_name
66+ dateexpected
67+ displayname
68+ distribution
69+ distroseries
70+ getTags
71+ getTagsData
72+ name
73 product
74- distribution
75+ product_release
76 productseries
77- distroseries
78- dateexpected
79- active
80- summary
81- target
82 series_target
83- displayname
84- title
85- bugtasks
86 specifications
87- product_release"/>
88+ summary
89+ target
90+ title
91+ "/>
92 <require
93 permission="launchpad.Edit"
94 attributes="
95@@ -1051,15 +1059,13 @@
96 setTags
97 "/>
98 <require
99- permission="zope.Public"
100- attributes="
101- getTags
102- getTagsData
103- "/>
104- <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
105- <allow
106+ permission="launchpad.View"
107+ interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
108+ <require
109+ permission="launchpad.View"
110 interface="lp.bugs.interfaces.bugtarget.IHasBugs"/>
111- <allow
112+ <require
113+ permission="launchpad.View"
114 interface="lp.bugs.interfaces.bugtarget.IHasOfficialBugTags"/>
115 <allow
116 interface="lp.app.interfaces.security.IAuthorization"/>
117@@ -1070,10 +1076,11 @@
118
119 <!-- IStructuralSubscriptionTarget -->
120
121- <allow
122+ <require
123+ permission="launchpad.View"
124 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
125 <require
126- permission="launchpad.AnyPerson"
127+ permission="launchpad.AnyAllowedPerson"
128 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
129
130 </class>
131
132=== modified file 'lib/lp/registry/interfaces/milestone.py'
133--- lib/lp/registry/interfaces/milestone.py 2012-10-15 02:32:30 +0000
134+++ lib/lp/registry/interfaces/milestone.py 2012-10-17 15:25:27 +0000
135@@ -267,6 +267,9 @@
136 why this is not a property.
137 """
138
139+ def userCanView(user):
140+ """True if the given user has access to this product."""
141+
142
143 # Avoid circular imports
144 IBugTask['milestone'].schema = IMilestone
145
146=== modified file 'lib/lp/registry/model/milestone.py'
147--- lib/lp/registry/model/milestone.py 2012-10-06 23:14:35 +0000
148+++ lib/lp/registry/model/milestone.py 2012-10-17 15:25:27 +0000
149@@ -351,6 +351,17 @@
150 from lp.registry.model.milestonetag import MilestoneTag
151 return list(self.getTagsData().values(MilestoneTag.tag))
152
153+ def userCanView(self, user):
154+ """See `IMilestone`."""
155+ # A database constraint ensures that either self.product
156+ # or self.distribution is not None.
157+ if self.product is None:
158+ # Distributions are always public, and so are their
159+ # milestones.
160+ return True
161+ # Delegate the permission check
162+ return self.product.userCanView(user)
163+
164
165 class MilestoneSet:
166 implements(IMilestoneSet)
167
168=== modified file 'lib/lp/registry/tests/test_milestone.py'
169--- lib/lp/registry/tests/test_milestone.py 2012-10-02 02:56:32 +0000
170+++ lib/lp/registry/tests/test_milestone.py 2012-10-17 15:25:27 +0000
171@@ -6,11 +6,20 @@
172 __metaclass__ = type
173
174 from operator import attrgetter
175+from storm.exceptions import NoneError
176 import unittest
177
178 from zope.component import getUtility
179+from zope.security.checker import (
180+ CheckerPublic,
181+ getChecker,
182+ )
183+from zope.security.interfaces import Unauthorized
184
185+from lp.app.enums import InformationType
186+from lp.app.interfaces.services import IService
187 from lp.app.errors import NotFoundError
188+from lp.registry.enums import SharingPermission
189 from lp.registry.interfaces.distribution import IDistributionSet
190 from lp.registry.interfaces.milestone import (
191 IHasMilestones,
192@@ -101,6 +110,244 @@
193 [1, 2, 3])
194
195
196+class MilestoneSecurityAdaperTestCase(TestCaseWithFactory):
197+ """A TestCase for the security adapter of milestones."""
198+
199+ layer = DatabaseFunctionalLayer
200+
201+ def setUp(self):
202+ super(MilestoneSecurityAdaperTestCase, self).setUp()
203+ self.public_product = self.factory.makeProduct()
204+ self.public_milestone = self.factory.makeMilestone(
205+ product=self.public_product)
206+ self.proprietary_product_owner = self.factory.makePerson()
207+ self.proprietary_product = self.factory.makeProduct(
208+ owner=self.proprietary_product_owner,
209+ information_type=InformationType.PROPRIETARY)
210+ self.proprietary_milestone = self.factory.makeMilestone(
211+ product=self.proprietary_product)
212+
213+ expected_get_permissions = {
214+ CheckerPublic: set((
215+ 'id', 'checkAuthenticated', 'checkUnauthenticated',
216+ 'userCanView',
217+ )),
218+ 'launchpad.View': set((
219+ 'active', 'bug_subscriptions', 'bugtasks', 'code_name',
220+ 'dateexpected', 'displayname', 'distribution', 'distroseries',
221+ '_getOfficialTagClause', 'getBugSummaryContextWhereClause',
222+ 'getBugTaskWeightFunction', 'getSubscription',
223+ 'getSubscriptions', 'getTags', 'getTagsData',
224+ 'getUsedBugTagsWithOpenCounts', 'name', 'official_bug_tags',
225+ 'parent_subscription_target', 'product', 'product_release',
226+ 'productseries', 'searchTasks', 'series_target',
227+ 'specifications', 'summary', 'target', 'target_type_display',
228+ 'title', 'userCanAlterBugSubscription',
229+ 'userCanAlterSubscription', 'userHasBugSubscriptions',
230+ )),
231+ 'launchpad.AnyAllowedPerson': set((
232+ 'addBugSubscription', 'addBugSubscriptionFilter',
233+ 'addSubscription', 'removeBugSubscription',
234+ )),
235+ 'launchpad.Edit': set((
236+ 'closeBugsAndBlueprints', 'createProductRelease',
237+ 'destroySelf', 'setTags',
238+ )),
239+ }
240+
241+ def test_get_permissions(self):
242+ milestone = self.factory.makeMilestone()
243+ checker = getChecker(milestone)
244+ self.checkPermissions(
245+ self.expected_get_permissions, checker.get_permissions, 'get')
246+
247+ expected_set_permissions = {
248+ 'launchpad.Edit': set((
249+ 'active', 'code_name', 'dateexpected', 'distroseries', 'name',
250+ 'product_release', 'productseries', 'summary',
251+ )),
252+ }
253+
254+ def test_set_permissions(self):
255+ milestone = self.factory.makeMilestone()
256+ checker = getChecker(milestone)
257+ self.checkPermissions(
258+ self.expected_set_permissions, checker.set_permissions, 'set')
259+
260+ def assertAccessAuthorized(self, attribute_names, obj):
261+ # Try to access the given attributes of obj. No exception
262+ # should be raised.
263+ for name in attribute_names:
264+ # class Milestone does not implement all attributes defined by
265+ # class IMilestone. AttributeErrors caused by attempts to
266+ # access these attribues are not relevant here: We simply
267+ # want to be sure that no Unauthorized error is raised.
268+ try:
269+ getattr(obj, name)
270+ except AttributeError:
271+ pass
272+
273+ def assertAccessUnauthorized(self, attribute_names, obj):
274+ # Try to access the given attributes of obj. Unauthorized
275+ # should be raised.
276+ for name in attribute_names:
277+ self.assertRaises(Unauthorized, getattr, obj, name)
278+
279+ def assertChangeAuthorized(self, attribute_names, obj):
280+ # Try to changes the given attributes of obj. Unauthorized
281+ # should be raised.
282+ for name in attribute_names:
283+ # Not all attributes declared in configure.zcml to be
284+ # settable actually exist. Attempts to set them raises
285+ # an AttributeError. Setting an Attribute to None may not
286+ # be allowed.
287+ #
288+ # Both errors can be ignored here: This method intends only
289+ # to prove that Unauthorized is not raised.
290+ try:
291+ setattr(obj, name, None)
292+ except (AttributeError, NoneError):
293+ pass
294+
295+ def assertChangeUnauthorized(self, attribute_names, obj):
296+ # Try to changes the given attributes of obj. Unauthorized
297+ # should be raised.
298+ for name in attribute_names:
299+ self.assertRaises(Unauthorized, setattr, obj, name, None)
300+
301+ def test_access_for_anonymous(self):
302+ # Anonymous users have access to to public attributes of
303+ # milestones for private and public products.
304+ with person_logged_in(ANONYMOUS):
305+ self.assertAccessAuthorized(
306+ self.expected_get_permissions[CheckerPublic],
307+ self.public_milestone)
308+ self.assertAccessAuthorized(
309+ self.expected_get_permissions[CheckerPublic],
310+ self.proprietary_milestone)
311+
312+ # They have access to attributes requiring the permission
313+ # launchpad.View of milestones for public products...
314+ self.assertAccessAuthorized(
315+ self.expected_get_permissions['launchpad.View'],
316+ self.public_milestone)
317+
318+ # ...but not to the same attributes of milestones for private
319+ # products.
320+ self.assertAccessUnauthorized(
321+ self.expected_get_permissions['launchpad.View'],
322+ self.proprietary_milestone)
323+
324+ # They cannot access other attributes.
325+ for permission, names in self.expected_get_permissions.items():
326+ if permission in (CheckerPublic, 'launchpad.View'):
327+ continue
328+ self.assertAccessUnauthorized(names, self.public_milestone)
329+ self.assertAccessUnauthorized(
330+ names, self.proprietary_milestone)
331+
332+ # They cannot change any attributes.
333+ for permission, names in self.expected_set_permissions.items():
334+ self.assertChangeUnauthorized(names, self.public_milestone)
335+ self.assertChangeUnauthorized(
336+ names, self.proprietary_milestone)
337+
338+ def test_access_for_ordinary_user(self):
339+ # Regular users have to public attributes of milestones for
340+ # private and public products.
341+ user = self.factory.makePerson()
342+ with person_logged_in(user):
343+ self.assertAccessAuthorized(
344+ self.expected_get_permissions[CheckerPublic],
345+ self.public_milestone)
346+ self.assertAccessAuthorized(
347+ self.expected_get_permissions[CheckerPublic],
348+ self.proprietary_milestone)
349+
350+ # They have access to attributes requiring the permission
351+ # launchpad.View or launchpad.AnyAllowedPerson of milestones
352+ # for public products...
353+ self.assertAccessAuthorized(
354+ self.expected_get_permissions['launchpad.View'],
355+ self.public_milestone)
356+ self.assertAccessAuthorized(
357+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
358+ self.public_milestone)
359+
360+ # ...but not to the same attributes of milestones for private
361+ # products.
362+ self.assertAccessUnauthorized(
363+ self.expected_get_permissions['launchpad.View'],
364+ self.proprietary_milestone)
365+ self.assertAccessUnauthorized(
366+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
367+ self.proprietary_milestone)
368+
369+ # They cannot access other attributes.
370+ for permission, names in self.expected_get_permissions.items():
371+ if permission in (
372+ CheckerPublic, 'launchpad.View',
373+ 'launchpad.AnyAllowedPerson'):
374+ continue
375+ self.assertAccessUnauthorized(names, self.public_milestone)
376+ self.assertAccessUnauthorized(
377+ names, self.proprietary_milestone)
378+
379+ # They cannot change attributes.
380+ for permission, names in self.expected_set_permissions.items():
381+ self.assertChangeUnauthorized(names, self.public_milestone)
382+ self.assertChangeUnauthorized(
383+ names, self.proprietary_milestone)
384+
385+ def test_access_for_user_with_grant_for_private_product(self):
386+ # Users with a policy grant for a private product have access
387+ # to most attributes of the private product.
388+ user = self.factory.makePerson()
389+ with person_logged_in(self.proprietary_product_owner):
390+ getUtility(IService, 'sharing').sharePillarInformation(
391+ self.proprietary_product, user, self.proprietary_product_owner,
392+ {InformationType.PROPRIETARY: SharingPermission.ALL})
393+
394+ with person_logged_in(user):
395+ self.assertAccessAuthorized(
396+ self.expected_get_permissions[CheckerPublic],
397+ self.proprietary_milestone)
398+
399+ # They have access to attributes requiring the permission
400+ # launchpad.View or launchpad.AnyAllowedPerson of milestones
401+ # for the private product.
402+ self.assertAccessAuthorized(
403+ self.expected_get_permissions['launchpad.View'],
404+ self.proprietary_milestone)
405+ self.assertAccessAuthorized(
406+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
407+ self.proprietary_milestone)
408+
409+ # They cannot access other attributes.
410+ for permission, names in self.expected_get_permissions.items():
411+ if permission in (
412+ CheckerPublic, 'launchpad.View',
413+ 'launchpad.AnyAllowedPerson'):
414+ continue
415+ self.assertAccessUnauthorized(
416+ names, self.proprietary_milestone)
417+
418+ # They cannot change attributes.
419+ for names in self.expected_set_permissions.values():
420+ self.assertChangeUnauthorized(
421+ names, self.proprietary_milestone)
422+
423+ def test_access_for_product_owner(self):
424+ # The owner of a private product can access all attributes.
425+ with person_logged_in(self.proprietary_product_owner):
426+ for names in self.expected_get_permissions.values():
427+ self.assertAccessAuthorized(names, self.proprietary_milestone)
428+
429+ # They can change attributes.
430+ for permission, names in self.expected_set_permissions.items():
431+ self.assertChangeAuthorized(names, self.proprietary_milestone)
432+
433+
434 class HasMilestonesSnapshotTestCase(TestCaseWithFactory):
435 """A TestCase for snapshots of pillars with milestones."""
436
437
438=== modified file 'lib/lp/registry/tests/test_person.py'
439--- lib/lp/registry/tests/test_person.py 2012-10-16 23:42:55 +0000
440+++ lib/lp/registry/tests/test_person.py 2012-10-17 15:25:27 +0000
441@@ -1336,17 +1336,21 @@
442 # product.owner, not the team.
443 product = self.factory.makeProduct(
444 information_type=InformationType.PROPRIETARY)
445- milestone = self.factory.makeMilestone(
446- dateexpected=self.current_milestone.dateexpected, product=product)
447- spec = self.factory.makeSpecification(
448- milestone=milestone, information_type=InformationType.PROPRIETARY)
449- workitem = self.factory.makeSpecificationWorkItem(
450- specification=spec, assignee=self.team.teamowner)
451- workitems = self.team.getAssignedSpecificationWorkItemsDueBefore(
452- milestone.dateexpected, self.team)
453+ with person_logged_in(removeSecurityProxy(product).owner):
454+ milestone = self.factory.makeMilestone(
455+ dateexpected=self.current_milestone.dateexpected,
456+ product=product)
457+ spec = self.factory.makeSpecification(
458+ milestone=milestone,
459+ information_type=InformationType.PROPRIETARY)
460+ workitem = self.factory.makeSpecificationWorkItem(
461+ specification=spec, assignee=self.team.teamowner)
462+ workitems = self.team.getAssignedSpecificationWorkItemsDueBefore(
463+ milestone.dateexpected, self.team)
464 self.assertNotIn(workitem, workitems)
465 workitems = self.team.getAssignedSpecificationWorkItemsDueBefore(
466- milestone.dateexpected, removeSecurityProxy(product).owner)
467+ removeSecurityProxy(milestone).dateexpected,
468+ removeSecurityProxy(product).owner)
469 self.assertIn(workitem, workitems)
470
471 def _makeProductSpec(self, milestone_dateexpected):
472
473=== modified file 'lib/lp/registry/tests/test_product.py'
474--- lib/lp/registry/tests/test_product.py 2012-10-16 00:57:45 +0000
475+++ lib/lp/registry/tests/test_product.py 2012-10-17 15:25:27 +0000
476@@ -537,28 +537,6 @@
477 CannotChangeInformationType, 'Some series are packaged.'):
478 product.information_type = InformationType.PROPRIETARY
479
480- def check_permissions(self, expected_permissions, used_permissions,
481- type_):
482- expected = set(expected_permissions.keys())
483- self.assertEqual(
484- expected, set(used_permissions.values()),
485- 'Unexpected %s permissions' % type_)
486- for permission in expected_permissions:
487- attribute_names = set(
488- name for name, value in used_permissions.items()
489- if value == permission)
490- self.assertEqual(
491- expected_permissions[permission], attribute_names,
492- 'Unexpected set of attributes with %s permission %s:\n'
493- 'Defined but not expected: %s\n'
494- 'Expected but not defined: %s'
495- % (
496- type_, permission,
497- sorted(
498- attribute_names - expected_permissions[permission]),
499- sorted(
500- expected_permissions[permission] - attribute_names)))
501-
502 expected_get_permissions = {
503 CheckerPublic: set((
504 'active', 'id', 'information_type', 'pillar_category', 'private',
505@@ -647,7 +625,7 @@
506 def test_get_permissions(self):
507 product = self.factory.makeProduct()
508 checker = getChecker(product)
509- self.check_permissions(
510+ self.checkPermissions(
511 self.expected_get_permissions, checker.get_permissions, 'get')
512
513 def test_set_permissions(self):
514@@ -680,7 +658,7 @@
515 }
516 product = self.factory.makeProduct()
517 checker = getChecker(product)
518- self.check_permissions(
519+ self.checkPermissions(
520 expected_set_permissions, checker.set_permissions, 'set')
521
522 def test_access_launchpad_View_public_product(self):
523
524=== modified file 'lib/lp/security.py'
525--- lib/lp/security.py 2012-10-12 14:53:10 +0000
526+++ lib/lp/security.py 2012-10-17 15:25:27 +0000
527@@ -510,7 +510,7 @@
528 usedfor = IDistributionMirror
529
530
531-class ViewMilestone(AnonymousAuthorization):
532+class ViewAbstractMilestone(AnonymousAuthorization):
533 """Anyone can view an IMilestone or an IProjectGroupMilestone."""
534 usedfor = IAbstractMilestone
535
536@@ -727,6 +727,25 @@
537 return False
538
539
540+class ViewMilestone(AuthorizationBase):
541+ permission = 'launchpad.View'
542+ usedfor = IMilestone
543+
544+ def checkAuthenticated(self, user):
545+ return self.obj.userCanView(user)
546+
547+ def checkUnauthenticated(self):
548+ return self.obj.userCanView(user=None)
549+
550+
551+class EditMilestone(ViewMilestone):
552+ permission = 'launchpad.AnyAllowedPerson'
553+ usedfor = IMilestone
554+
555+ def checkUnauthenticated(self):
556+ return False
557+
558+
559 class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
560 permission = 'launchpad.Edit'
561 usedfor = IMilestone
562
563=== modified file 'lib/lp/testing/__init__.py'
564--- lib/lp/testing/__init__.py 2012-09-05 14:44:17 +0000
565+++ lib/lp/testing/__init__.py 2012-10-17 15:25:27 +0000
566@@ -704,6 +704,36 @@
567 raise AssertionError(
568 'string %r does not end with %r' % (s, suffix))
569
570+ def checkPermissions(self, expected_permissions, used_permissions,
571+ type_):
572+ """Check if the used_permissions match expected_permissions.
573+
574+ :param expected_permissions: A dictionary mapping a permission
575+ to a set of attribute names.
576+ :param used_permissions: The property get_permissions or
577+ set_permissions of getChecker(security_proxied_object).
578+ :param type_: The string "set" or "get".
579+ """
580+ expected = set(expected_permissions.keys())
581+ self.assertEqual(
582+ expected, set(used_permissions.values()),
583+ 'Unexpected %s permissions' % type_)
584+ for permission in expected_permissions:
585+ attribute_names = set(
586+ name for name, value in used_permissions.items()
587+ if value == permission)
588+ self.assertEqual(
589+ expected_permissions[permission], attribute_names,
590+ 'Unexpected set of attributes with %s permission %s:\n'
591+ 'Defined but not expected: %s\n'
592+ 'Expected but not defined: %s'
593+ % (
594+ type_, permission,
595+ sorted(
596+ attribute_names - expected_permissions[permission]),
597+ sorted(
598+ expected_permissions[permission] - attribute_names)))
599+
600
601 class TestCaseWithFactory(TestCase):
602