Merge lp:~adeuring/launchpad/specification-auth-check into lp:launchpad

Proposed by Abel Deuring on 2012-08-20
Status: Merged
Approved by: Abel Deuring on 2012-08-21
Approved revision: no longer in the source branch.
Merged at revision: 15844
Proposed branch: lp:~adeuring/launchpad/specification-auth-check
Merge into: lp:launchpad
Diff against target: 685 lines (+379/-34)
8 files modified
lib/lp/blueprints/browser/tests/test_specificationdependency.py (+13/-3)
lib/lp/blueprints/configure.zcml (+17/-5)
lib/lp/blueprints/interfaces/specification.py (+29/-9)
lib/lp/blueprints/model/specification.py (+27/-0)
lib/lp/blueprints/tests/test_specification.py (+235/-1)
lib/lp/blueprints/tests/test_webservice.py (+31/-16)
lib/lp/permissions.zcml (+5/-0)
lib/lp/security.py (+22/-0)
To merge this branch: bzr merge lp:~adeuring/launchpad/specification-auth-check
Reviewer Review Type Date Requested Status
Francesco Banconi (community) 2012-08-20 Approve on 2012-08-21
Review via email: mp+120430@code.launchpad.net

Commit Message

Prepare permission checks for ISPecification for access grants.

Description of the Change

This branch changes the security adapters for blueprints so that
the visibility of a blueprint can be limited to certain users as
described in https://dev.launchpad.net/LEP/PrivateProjects

Real sharing, as already implemented for bugs and branches, is not
yet possible.

The core changes are:

- the new class ISpecificationView. This class contains most
  properties which were before defined in ISpecificationPublic.
- ISpecificationPublic now defines only "unsensitive data":
  database ID, information_type and a new method userCanView().

  userCanView() is used to check if a user may access the
  specifcation.

- new security adapters ViewSpecification and EditWhiteboardSpecification.

- a new permission "launchpad.AnyAllowedPerson"

The current access rules are:

- Everybody, including anonymous users, can view every specification.
- Each logged in user can change the whiteboard.
- changes of some properties required the permission launchpad.Admin.
- changes of most properties require the permission launchpad.Edit.

This branch does not change the security adapters for launchpad.Admin
and launchpad.Edit.

The permissions for read access of ISpecificationView and for changes
of the whiteboard attribute of course need future changes. The final
rule will be: If a project is not publicly visible, only persons which
have access grants to the specification may view the data and change
the whiteboard.

Since the access grant related tables cannot yet store grants for
specifications, the current implementation of userCanView() simply
checks if the specification is publicly visible; if not, only those
people who can also edit most properties are granted view access.

Note that the default value of Specification.information_type is
1 (or InformationType.PUBLIC) and that the value cannot be changed
at present, so the "practical access rules" did not change.

Some notes about implementation details:

- Using the permission launchpad.View instead of launchpad.LimitedView
  in

    <require
        permission="launchpad.LimitedView"
        interface="lp.blueprints.interfaces.specification.ISpecificationView" />

  does not work: lp/secruity.py defined another Adapter for this
  permission:

  class AnonymousAccessToISpecificationPublic(AnonymousAuthorization):
      """Anonymous users have launchpad.View on ISpecificationPublic.

      This is only needed because lazr.restful is hard-coded to check that
      permission before returning things in a collection.
      """

      permission = 'launchpad.View'
      usedfor = ISpecificationPublic

- Changes to ISpecification.whiteboard required the permission
  launchpad.AnyPerson. This permission can longer be used because there
  is a special "short-cut logic" for this permission in
  lp.services.webapp.autorization.LaunchpadSecurityPolicy.checkPermission():

        if (permission == 'launchpad.AnyPerson' and
            ILaunchpadPrincipal.providedBy(principal)):
            return True

  In other words: No security adapters are used for this permission.

  So I added a new permission "launchpad.AnyAllowedPerson". Probably
  not the best name -- I'd be grateful for a better suggestion.

- The permission definitions for ISpecificationn are quite complex,

  To get an overview about all permissions and their adapters,
  I added the tests test_get_permissions(), test_set_permissions()
  test_security_adapters().

  Only when I wrote these tests, I noticed this config directive:

    <require
        permission="launchpad.AnyPerson"
        attributes="linkBug
                    unlinkBug
                    setWorkItems"/>
  That is of course easy to change -- but I missed it at first...

- The tests test_special_user_read_access(), test_special_user_write_access()
  should not only test that the owner of a specification has "special
  right", but that other people certain roles have these right too.

  I will add such tests in a later branch -- the diff for this one is
  already long enough, and there are already some tests, for example in
  ./stories/blueprints/xx-dependencies.txt.

- I had to change some existing tests:
  lib/lp/blueprints/browser/tests/test_specificationdependency.py and
  lib/lp/blueprints/tests/test_webservice.py. These tests issue some
  browser requests and want to access some attributes of a specification
  when the request finished. During the end of a request, the function
  endInteraction() is called which deletes thread_local.interaction --
  but the new security adapters need access to the interaction.

  The fixes are trivel: Aadding "with_person_logged_in()" is often enough;
  in some cases it is possible to access a specification attribute before
  the request is issued.

tests:

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

no lint.

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

The code looks good Abel: this is a nice and well tested incremental change. Unfortunately I don't have a brilliant suggestion for the name of the new permission.
The tests pass, the new ones and the changed ones. Some comments follow.

286 + def check_permissions(self, expected_permissions, used_permissions,
287 + type_):

It seems that indentation should be fixed here.

343 + def test_set_permissions(self):
344 + expected_get_permissions = {

Maybe you intended expected_set_permissions?

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/browser/tests/test_specificationdependency.py'
2--- lib/lp/blueprints/browser/tests/test_specificationdependency.py 2012-01-01 02:58:52 +0000
3+++ lib/lp/blueprints/browser/tests/test_specificationdependency.py 2012-08-20 16:43:20 +0000
4@@ -9,7 +9,10 @@
5 __metaclass__ = type
6
7 from lp.services.webapp import canonical_url
8-from lp.testing import BrowserTestCase
9+from lp.testing import (
10+ BrowserTestCase,
11+ person_logged_in,
12+ )
13 from lp.testing.layers import DatabaseFunctionalLayer
14
15
16@@ -21,7 +24,14 @@
17 # field of the form to add a dependency to a spec.
18 spec = self.factory.makeSpecification(owner=self.user)
19 dependency = self.factory.makeSpecification()
20+ dependency_url = canonical_url(dependency)
21 browser = self.getViewBrowser(spec, '+linkdependency')
22- browser.getControl('Depends On').value = canonical_url(dependency)
23+ browser.getControl('Depends On').value = dependency_url
24 browser.getControl('Continue').click()
25- self.assertIn(dependency, spec.dependencies)
26+ # click() above issues a request, and
27+ # ZopePublication.endRequest() calls
28+ # zope.security.management.endInteraction().
29+ # We need a new interaction for the permission checks
30+ # on ISpecification objects.
31+ with person_logged_in(None):
32+ self.assertIn(dependency, spec.dependencies)
33
34=== modified file 'lib/lp/blueprints/configure.zcml'
35--- lib/lp/blueprints/configure.zcml 2012-05-17 07:46:56 +0000
36+++ lib/lp/blueprints/configure.zcml 2012-08-20 16:43:20 +0000
37@@ -149,9 +149,20 @@
38
39 <class class="lp.blueprints.model.specification.Specification">
40 <allow interface="lp.blueprints.interfaces.specification.ISpecificationPublic"/>
41- <!-- We allow any authenticated person to update the whiteboard -->
42- <require
43- permission="launchpad.AnyPerson"
44+ <require
45+ permission="launchpad.Edit"
46+ set_attributes="information_type" />
47+
48+ <require
49+ permission="launchpad.LimitedView"
50+ interface="lp.blueprints.interfaces.specification.ISpecificationView" />
51+
52+ <!-- For publicly visible specifications, we allow any authenticated
53+ person to update the whiteboard. The whiteboard of restricted
54+ specification may only be changed by people who may also view
55+ the specification. -->
56+ <require
57+ permission="launchpad.AnyAllowedPerson"
58 set_attributes="whiteboard"/>
59 <!-- NB: goals and goalstatus are not to be set directly, it should
60 only be set through the proposeGoal / acceptBy / declineBy
61@@ -166,11 +177,12 @@
62 <require
63 permission="launchpad.Admin"
64 set_attributes="priority direction_approved"/>
65- <allow
66+ <require
67+ permission="launchpad.LimitedView"
68 attributes="bugs
69 bug_links"/>
70 <require
71- permission="launchpad.AnyPerson"
72+ permission="launchpad.AnyAllowedPerson"
73 attributes="linkBug
74 unlinkBug
75 setWorkItems"/>
76
77=== modified file 'lib/lp/blueprints/interfaces/specification.py'
78--- lib/lp/blueprints/interfaces/specification.py 2012-05-17 07:46:56 +0000
79+++ lib/lp/blueprints/interfaces/specification.py 2012-08-20 16:43:20 +0000
80@@ -9,9 +9,10 @@
81
82 __all__ = [
83 'ISpecification',
84+ 'ISpecificationDelta',
85 'ISpecificationPublic',
86 'ISpecificationSet',
87- 'ISpecificationDelta',
88+ 'ISpecificationView',
89 ]
90
91
92@@ -47,6 +48,7 @@
93 )
94
95 from lp import _
96+from lp.app.interfaces.launchpad import IPrivacy
97 from lp.app.validators import LaunchpadValidationError
98 from lp.app.validators.url import valid_webref
99 from lp.blueprints.enums import (
100@@ -70,6 +72,7 @@
101 from lp.blueprints.interfaces.sprint import ISprint
102 from lp.bugs.interfaces.buglink import IBugLinkTarget
103 from lp.code.interfaces.branchlink import IHasLinkedBranches
104+from lp.registry.enums import InformationType
105 from lp.registry.interfaces.milestone import IMilestone
106 from lp.registry.interfaces.person import IPerson
107 from lp.registry.interfaces.projectgroup import IProjectGroup
108@@ -147,11 +150,28 @@
109 specification.title))
110
111
112-class ISpecificationPublic(IHasOwner, IHasLinkedBranches):
113+class ISpecificationPublic(IPrivacy):
114 """Specification's public attributes and methods."""
115
116 id = Int(title=_("Database ID"), required=True, readonly=True)
117
118+ information_type = exported(
119+ Choice(
120+ title=_('Information Type'), vocabulary=InformationType,
121+ required=True, readonly=True,
122+ description=_(
123+ 'The type of information contained in this bug report.')))
124+
125+ def userCanView(user):
126+ """Return True if `user` can see this ISpecification, false otherwise.
127+ """
128+
129+
130+class ISpecificationView(IHasOwner, IHasLinkedBranches):
131+ """Specification's attributes and methods that require
132+ the permission launchpad.LimitedView.
133+ """
134+
135 name = exported(
136 SpecNameField(
137 title=_('Name'), required=True, readonly=False,
138@@ -550,21 +570,21 @@
139 """Specification's attributes and methods protected with launchpad.Edit.
140 """
141
142- @mutator_for(ISpecificationPublic['definition_status'])
143+ @mutator_for(ISpecificationView['definition_status'])
144 @call_with(user=REQUEST_USER)
145 @operation_parameters(
146 definition_status=copy_field(
147- ISpecificationPublic['definition_status']))
148+ ISpecificationView['definition_status']))
149 @export_write_operation()
150 @operation_for_version("devel")
151 def setDefinitionStatus(definition_status, user):
152 """Mutator for definition_status that calls updateLifeCycle."""
153
154- @mutator_for(ISpecificationPublic['implementation_status'])
155+ @mutator_for(ISpecificationView['implementation_status'])
156 @call_with(user=REQUEST_USER)
157 @operation_parameters(
158 implementation_status=copy_field(
159- ISpecificationPublic['implementation_status']))
160+ ISpecificationView['implementation_status']))
161 @export_write_operation()
162 @operation_for_version("devel")
163 def setImplementationStatus(implementation_status, user):
164@@ -602,13 +622,13 @@
165 """
166
167
168-class ISpecification(ISpecificationPublic, ISpecificationEditRestricted,
169- IBugLinkTarget):
170+class ISpecification(ISpecificationPublic, ISpecificationView,
171+ ISpecificationEditRestricted, IBugLinkTarget):
172 """A Specification."""
173
174 export_as_webservice_entry(as_of="beta")
175
176- @mutator_for(ISpecificationPublic['workitems_text'])
177+ @mutator_for(ISpecificationView['workitems_text'])
178 @operation_parameters(new_work_items=WorkItemsText())
179 @export_write_operation()
180 @operation_for_version('devel')
181
182=== modified file 'lib/lp/blueprints/model/specification.py'
183--- lib/lp/blueprints/model/specification.py 2012-08-07 02:31:56 +0000
184+++ lib/lp/blueprints/model/specification.py 2012-08-20 16:43:20 +0000
185@@ -66,6 +66,11 @@
186 from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
187 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
188 from lp.bugs.model.buglinktarget import BugLinkTargetMixin
189+from lp.registry.enums import (
190+ InformationType,
191+ PRIVATE_INFORMATION_TYPES,
192+ PUBLIC_INFORMATION_TYPES,
193+ )
194 from lp.registry.interfaces.distribution import IDistribution
195 from lp.registry.interfaces.distroseries import IDistroSeries
196 from lp.registry.interfaces.person import validate_public_person
197@@ -210,6 +215,8 @@
198 blocked_specs = SQLRelatedJoin('Specification', joinColumn='dependency',
199 otherColumn='specification', orderBy='title',
200 intermediateTable='SpecificationDependency')
201+ information_type = EnumCol(
202+ enum=InformationType, notNull=True, default=InformationType.PUBLIC)
203
204 @cachedproperty
205 def subscriptions(self):
206@@ -809,6 +816,26 @@
207 return '<Specification %s %r for %r>' % (
208 self.id, self.name, self.target.name)
209
210+ @property
211+ def private(self):
212+ return self.information_type in PRIVATE_INFORMATION_TYPES
213+
214+ def userCanView(self, user):
215+ """See `ISpecification`."""
216+ if self.information_type in PUBLIC_INFORMATION_TYPES:
217+ return True
218+ if user is None:
219+ return False
220+ # Temporary: we should access the grant tables instead of
221+ # checking if a given user has special roles.
222+ # The following is basically copied from
223+ # EditSpecificationByRelatedPeople.checkAuthenticated()
224+ return (user.in_admin or
225+ user.isOwner(self.target) or
226+ user.isOneOfDrivers(self.target) or
227+ user.isOneOf(
228+ self, ['owner', 'drafter', 'assignee', 'approver']))
229+
230
231 class HasSpecificationsMixin:
232 """A mixin class that implements many of the common shortcut properties
233
234=== modified file 'lib/lp/blueprints/tests/test_specification.py'
235--- lib/lp/blueprints/tests/test_specification.py 2012-02-29 13:03:19 +0000
236+++ lib/lp/blueprints/tests/test_specification.py 2012-08-20 16:43:20 +0000
237@@ -6,10 +6,18 @@
238 __metaclass__ = type
239
240
241-from zope.component import getUtility
242+from zope.component import (
243+ getUtility,
244+ queryAdapter,
245+ )
246+from zope.security.checker import (
247+ CheckerPublic,
248+ getChecker,
249+ )
250 from zope.security.interfaces import Unauthorized
251 from zope.security.proxy import removeSecurityProxy
252
253+from lp.app.interfaces.security import IAuthorization
254 from lp.blueprints.enums import (
255 NewSpecificationDefinitionStatus,
256 SpecificationDefinitionStatus,
257@@ -17,12 +25,24 @@
258 )
259 from lp.blueprints.errors import TargetAlreadyHasSpecification
260 from lp.blueprints.interfaces.specification import ISpecificationSet
261+from lp.registry.enums import (
262+ PRIVATE_INFORMATION_TYPES,
263+ PUBLIC_INFORMATION_TYPES,
264+ )
265 from lp.services.webapp.authorization import check_permission
266+from lp.security import (
267+ AdminSpecification,
268+ EditSpecificationByRelatedPeople,
269+ EditWhiteboardSpecification,
270+ ViewSpecification,
271+ )
272 from lp.testing import (
273 login_person,
274+ person_logged_in,
275 TestCaseWithFactory,
276 )
277 from lp.testing.layers import DatabaseFunctionalLayer
278+from lp.services.webapp.interaction import ANONYMOUS
279
280
281 class SpecificationTests(TestCaseWithFactory):
282@@ -107,6 +127,220 @@
283 self.assertRaises(
284 Unauthorized, getattr, specification, 'setTarget')
285
286+ def check_permissions(self, expected_permissions, used_permissions,
287+ type_):
288+ expected = set(expected_permissions.keys())
289+ self.assertEqual(
290+ expected, set(used_permissions.values()),
291+ 'Unexpected %s permissions' % type_)
292+ for permission in expected_permissions:
293+ attribute_names = set(
294+ name for name, value in used_permissions.items()
295+ if value == permission)
296+ self.assertEqual(
297+ expected_permissions[permission], attribute_names,
298+ 'Unexpected set of attributes with %s permission %s:\n'
299+ 'Defined but not expected: %s\n'
300+ 'Expected but not defined: %s'
301+ % (
302+ type_, permission,
303+ attribute_names - expected_permissions[permission],
304+ expected_permissions[permission] - attribute_names))
305+
306+ def test_get_permissions(self):
307+ expected_get_permissions = {
308+ CheckerPublic: set((
309+ 'id', 'information_type', 'private', 'userCanView')),
310+ 'launchpad.LimitedView': set((
311+ 'acceptBy', 'all_blocked', 'all_deps', 'approver',
312+ 'approverID', 'assignee', 'assigneeID', 'blocked_specs',
313+ 'bug_links', 'bugs', 'completer', 'createDependency',
314+ 'date_completed', 'date_goal_decided', 'date_goal_proposed',
315+ 'date_started', 'datecreated', 'declineBy',
316+ 'definition_status', 'dependencies', 'direction_approved',
317+ 'distribution', 'distroseries', 'drafter', 'drafterID',
318+ 'getBranchLink', 'getDelta', 'getLinkedBugTasks',
319+ 'getSprintSpecification', 'getSubscriptionByName', 'goal',
320+ 'goal_decider', 'goal_proposer', 'goalstatus',
321+ 'has_accepted_goal', 'implementation_status', 'informational',
322+ 'isSubscribed', 'is_blocked', 'is_complete', 'is_incomplete',
323+ 'is_started', 'lifecycle_status', 'linkBranch', 'linkSprint',
324+ 'linked_branches', 'man_days', 'milestone', 'name',
325+ 'notificationRecipientAddresses', 'owner', 'priority',
326+ 'product', 'productseries', 'proposeGoal', 'removeDependency',
327+ 'specurl', 'sprint_links', 'sprints', 'starter', 'subscribe',
328+ 'subscribers', 'subscription', 'subscriptions', 'summary',
329+ 'superseded_by', 'target', 'title', 'unlinkBranch',
330+ 'unlinkSprint', 'unsubscribe', 'updateLifecycleStatus',
331+ 'validateMove', 'whiteboard', 'work_items', 'workitems_text')),
332+ 'launchpad.Edit': set((
333+ 'newWorkItem', 'retarget', 'setDefinitionStatus',
334+ 'setImplementationStatus', 'setTarget', 'updateWorkItems')),
335+ 'launchpad.AnyAllowedPerson': set((
336+ 'unlinkBug', 'linkBug', 'setWorkItems')),
337+ }
338+ specification = self.factory.makeSpecification()
339+ checker = getChecker(specification)
340+ self.check_permissions(
341+ expected_get_permissions, checker.get_permissions, 'get')
342+
343+ def test_set_permissions(self):
344+ expected_get_permissions = {
345+ 'launchpad.Admin': set(('direction_approved', 'priority')),
346+ 'launchpad.AnyAllowedPerson': set(('whiteboard', )),
347+ 'launchpad.Edit': set((
348+ 'approver', 'assignee', 'definition_status', 'distribution',
349+ 'drafter', 'implementation_status', 'information_type',
350+ 'man_days', 'milestone', 'name', 'product', 'specurl',
351+ 'summary', 'superseded_by', 'title')),
352+ }
353+ specification = self.factory.makeSpecification()
354+ checker = getChecker(specification)
355+ self.check_permissions(
356+ expected_get_permissions, checker.set_permissions, 'set')
357+
358+ def test_security_adapters(self):
359+ expected_adapters = {
360+ CheckerPublic: None,
361+ 'launchpad.Admin': AdminSpecification,
362+ 'launchpad.AnyAllowedPerson': EditWhiteboardSpecification,
363+ 'launchpad.Edit': EditSpecificationByRelatedPeople,
364+ 'launchpad.LimitedView': ViewSpecification,
365+ }
366+ specification = self.factory.makeSpecification()
367+ for permission in expected_adapters:
368+ adapter = queryAdapter(specification, IAuthorization, permission)
369+ expected_class = expected_adapters[permission]
370+ if expected_class is None:
371+ self.assertIsNone(
372+ adapter, 'No security adapter for %s' % permission)
373+ else:
374+ self.assertTrue(
375+ isinstance(adapter, expected_class),
376+ 'Invalid adapter for %s: %s' % (permission, adapter))
377+
378+ def read_access_to_ISpecificationView(self, user, specification,
379+ error_expected):
380+ # Access an attribute whose interface is defined in
381+ # ISPecificationView.
382+ with person_logged_in(user):
383+ if error_expected:
384+ self.assertRaises(
385+ Unauthorized, getattr, specification, 'name')
386+ else:
387+ # Just try to access an attribute. No execption should be
388+ # raised.
389+ specification.name
390+
391+ def write_access_to_ISpecificationView(self, user, specification,
392+ error_expected, attribute, value):
393+ # Access an attribute whose interface is defined in
394+ # ISPecificationView.
395+ with person_logged_in(user):
396+ if error_expected:
397+ self.assertRaises(
398+ Unauthorized, setattr, specification, attribute, value)
399+ else:
400+ # Just try to change an attribute. No execption should be
401+ # raised.
402+ setattr(specification, attribute, value)
403+
404+ def test_anon_read_access(self):
405+ # Anonymous users have access to public specifications...
406+ specification = self.factory.makeSpecification()
407+ for information_type in PUBLIC_INFORMATION_TYPES:
408+ with person_logged_in(specification.owner):
409+ specification.information_type = information_type
410+ self.read_access_to_ISpecificationView(
411+ ANONYMOUS, specification, error_expected=False)
412+ # ...but not to private specifications.
413+ for information_type in PRIVATE_INFORMATION_TYPES:
414+ with person_logged_in(specification.owner):
415+ specification.information_type = information_type
416+ self.read_access_to_ISpecificationView(
417+ ANONYMOUS, specification, error_expected=True)
418+
419+ def test_anon_write_access(self):
420+ # Anonymous users do not have write access to specifications.
421+ specification = self.factory.makeSpecification()
422+ for information_type in (PUBLIC_INFORMATION_TYPES +
423+ PRIVATE_INFORMATION_TYPES):
424+ with person_logged_in(specification.owner):
425+ specification.information_type = information_type
426+ self.write_access_to_ISpecificationView(
427+ ANONYMOUS, specification, error_expected=True,
428+ attribute='whiteboard', value='foo')
429+ self.write_access_to_ISpecificationView(
430+ ANONYMOUS, specification, error_expected=True,
431+ attribute='name', value='foo')
432+
433+ def test_ordinary_user_read_access(self):
434+ # Oridnary users have access to public specifications...
435+ specification = self.factory.makeSpecification()
436+ user = self.factory.makePerson()
437+ for information_type in PUBLIC_INFORMATION_TYPES:
438+ with person_logged_in(specification.owner):
439+ specification.information_type = information_type
440+ self.read_access_to_ISpecificationView(
441+ user, specification, error_expected=False)
442+ # ...but not to private specifications.
443+ for information_type in PRIVATE_INFORMATION_TYPES:
444+ with person_logged_in(specification.owner):
445+ specification.information_type = information_type
446+ self.read_access_to_ISpecificationView(
447+ user, specification, error_expected=True)
448+
449+ def test_ordinary_user_write_access(self):
450+ # Oridnary users can change the whiteborad of public specifications.
451+ # They cannot change other attributes.
452+ specification = self.factory.makeSpecification()
453+ user = self.factory.makePerson()
454+ for information_type in PUBLIC_INFORMATION_TYPES:
455+ with person_logged_in(specification.owner):
456+ specification.information_type = information_type
457+ self.write_access_to_ISpecificationView(
458+ user, specification, error_expected=False,
459+ attribute='whiteboard', value='foo')
460+ self.write_access_to_ISpecificationView(
461+ user, specification, error_expected=True,
462+ attribute='name', value='foo')
463+ # The cannot change any attribute of private specifcations.
464+ for information_type in PRIVATE_INFORMATION_TYPES:
465+ with person_logged_in(specification.owner):
466+ specification.information_type = information_type
467+ self.write_access_to_ISpecificationView(
468+ user, specification, error_expected=True,
469+ attribute='whiteboard', value='foo')
470+ self.write_access_to_ISpecificationView(
471+ user, specification, error_expected=True,
472+ attribute='name', value='foo')
473+
474+ def test_special_user_read_access(self):
475+ # Users with special privileges can aceess the attributes
476+ # of public and private specifcations.
477+ specification = self.factory.makeSpecification()
478+ for information_type in (PUBLIC_INFORMATION_TYPES +
479+ PRIVATE_INFORMATION_TYPES):
480+ with person_logged_in(specification.owner):
481+ specification.information_type = information_type
482+ self.read_access_to_ISpecificationView(
483+ specification.owner, specification, error_expected=False)
484+
485+ def test_special_user_write_access(self):
486+ # Users with special privileges can change the attributes
487+ # of public and private specifcations.
488+ specification = self.factory.makeSpecification()
489+ for information_type in (PUBLIC_INFORMATION_TYPES +
490+ PRIVATE_INFORMATION_TYPES):
491+ with person_logged_in(specification.owner):
492+ specification.information_type = information_type
493+ self.write_access_to_ISpecificationView(
494+ specification.owner, specification, error_expected=False,
495+ attribute='whiteboard', value='foo')
496+ self.write_access_to_ISpecificationView(
497+ specification.owner, specification, error_expected=False,
498+ attribute='name', value='foo')
499+
500
501 class TestSpecificationSet(TestCaseWithFactory):
502
503
504=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
505--- lib/lp/blueprints/tests/test_webservice.py 2012-04-16 03:38:00 +0000
506+++ lib/lp/blueprints/tests/test_webservice.py 2012-08-20 16:43:20 +0000
507@@ -34,8 +34,12 @@
508
509 def getSpecOnWebservice(self, spec_object):
510 launchpadlib = self.getLaunchpadlib()
511- return launchpadlib.load(
512- '/%s/+spec/%s' % (spec_object.target.name, spec_object.name))
513+ # Ensure that there is an interaction so that the security
514+ # checks for spec_object work.
515+ with person_logged_in(ANONYMOUS):
516+ url = '/%s/+spec/%s' % (spec_object.target.name, spec_object.name)
517+ result = launchpadlib.load(url)
518+ return result
519
520 def getPillarOnWebservice(self, pillar_obj):
521 launchpadlib = self.getLaunchpadlib()
522@@ -53,52 +57,59 @@
523 # it's ready for prime time.
524 spec = self.factory.makeSpecification()
525 user = self.factory.makePerson()
526+ url = '/%s/+spec/%s' % (spec.product.name, spec.name)
527 webservice = webservice_for_person(user)
528- response = webservice.get(
529- '/%s/+spec/%s' % (spec.product.name, spec.name))
530+ response = webservice.get(url)
531 expected_keys = [u'self_link', u'http_etag', u'resource_type_link',
532- u'web_link']
533+ u'web_link', u'information_type']
534 self.assertEqual(response.status, 200)
535 self.assertContentEqual(expected_keys, response.jsonBody().keys())
536
537 def test_representation_contains_name(self):
538 spec = self.factory.makeSpecification()
539+ spec_name = spec.name
540 spec_webservice = self.getSpecOnWebservice(spec)
541- self.assertEqual(spec.name, spec_webservice.name)
542+ self.assertEqual(spec_name, spec_webservice.name)
543
544 def test_representation_contains_target(self):
545 spec = self.factory.makeSpecification(
546 product=self.factory.makeProduct())
547+ spec_target = spec.target
548 spec_webservice = self.getSpecOnWebservice(spec)
549- self.assertEqual(spec.target.name, spec_webservice.target.name)
550+ self.assertEqual(spec_target.name, spec_webservice.target.name)
551
552 def test_representation_contains_title(self):
553 spec = self.factory.makeSpecification(title='Foo')
554+ spec_title = spec.title
555 spec_webservice = self.getSpecOnWebservice(spec)
556- self.assertEqual(spec.title, spec_webservice.title)
557+ self.assertEqual(spec_title, spec_webservice.title)
558
559 def test_representation_contains_specification_url(self):
560 spec = self.factory.makeSpecification(specurl='http://example.com')
561+ spec_specurl = spec.specurl
562 spec_webservice = self.getSpecOnWebservice(spec)
563- self.assertEqual(spec.specurl, spec_webservice.specification_url)
564+ self.assertEqual(spec_specurl, spec_webservice.specification_url)
565
566 def test_representation_contains_summary(self):
567 spec = self.factory.makeSpecification(summary='Foo')
568+ spec_summary = spec.summary
569 spec_webservice = self.getSpecOnWebservice(spec)
570- self.assertEqual(spec.summary, spec_webservice.summary)
571+ self.assertEqual(spec_summary, spec_webservice.summary)
572
573 def test_representation_contains_implementation_status(self):
574 spec = self.factory.makeSpecification()
575+ spec_implementation_status = spec.implementation_status
576 spec_webservice = self.getSpecOnWebservice(spec)
577 self.assertEqual(
578- spec.implementation_status.title,
579+ spec_implementation_status.title,
580 spec_webservice.implementation_status)
581
582 def test_representation_contains_definition_status(self):
583 spec = self.factory.makeSpecification()
584+ spec_definition_status = spec.definition_status
585 spec_webservice = self.getSpecOnWebservice(spec)
586 self.assertEqual(
587- spec.definition_status.title, spec_webservice.definition_status)
588+ spec_definition_status.title, spec_webservice.definition_status)
589
590 def test_representation_contains_assignee(self):
591 # Hard-code the person's name or else we'd need to set up a zope
592@@ -128,18 +139,21 @@
593
594 def test_representation_contains_priority(self):
595 spec = self.factory.makeSpecification()
596+ spec_priority = spec.priority
597 spec_webservice = self.getSpecOnWebservice(spec)
598- self.assertEqual(spec.priority.title, spec_webservice.priority)
599+ self.assertEqual(spec_priority.title, spec_webservice.priority)
600
601 def test_representation_contains_date_created(self):
602 spec = self.factory.makeSpecification()
603+ spec_datecreated = spec.datecreated
604 spec_webservice = self.getSpecOnWebservice(spec)
605- self.assertEqual(spec.datecreated, spec_webservice.date_created)
606+ self.assertEqual(spec_datecreated, spec_webservice.date_created)
607
608 def test_representation_contains_whiteboard(self):
609 spec = self.factory.makeSpecification(whiteboard='Test')
610+ spec_whiteboard = spec.whiteboard
611 spec_webservice = self.getSpecOnWebservice(spec)
612- self.assertEqual(spec.whiteboard, spec_webservice.whiteboard)
613+ self.assertEqual(spec_whiteboard, spec_webservice.whiteboard)
614
615 def test_representation_contains_workitems(self):
616 work_item = self.factory.makeSpecificationWorkItem()
617@@ -160,10 +174,11 @@
618 def test_representation_contains_dependencies(self):
619 spec = self.factory.makeSpecification()
620 spec2 = self.factory.makeSpecification()
621+ spec2_name = spec2.name
622 spec.createDependency(spec2)
623 spec_webservice = self.getSpecOnWebservice(spec)
624 self.assertEqual(1, spec_webservice.dependencies.total_size)
625- self.assertEqual(spec2.name, spec_webservice.dependencies[0].name)
626+ self.assertEqual(spec2_name, spec_webservice.dependencies[0].name)
627
628 def test_representation_contains_linked_branches(self):
629 spec = self.factory.makeSpecification()
630
631=== modified file 'lib/lp/permissions.zcml'
632--- lib/lp/permissions.zcml 2011-12-24 16:28:37 +0000
633+++ lib/lp/permissions.zcml 2012-08-20 16:43:20 +0000
634@@ -20,6 +20,11 @@
635 access_level="write" />
636
637 <permission
638+ id="launchpad.AnyAllowedPerson"
639+ title="Any Authenticated Person for public data; any person having grants on restricted objects."
640+ access_level="write" />
641+
642+ <permission
643 id="launchpad.Edit" title="Editing something" access_level="write" />
644
645 <!-- Request large downloads, or other heavyweight jobs that are not
646
647=== modified file 'lib/lp/security.py'
648--- lib/lp/security.py 2012-08-16 13:27:16 +0000
649+++ lib/lp/security.py 2012-08-20 16:43:20 +0000
650@@ -45,6 +45,7 @@
651 from lp.blueprints.interfaces.specification import (
652 ISpecification,
653 ISpecificationPublic,
654+ ISpecificationView,
655 )
656 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
657 from lp.blueprints.interfaces.specificationsubscription import (
658@@ -519,6 +520,27 @@
659 usedfor = ISpecificationPublic
660
661
662+class ViewSpecification(AuthorizationBase):
663+
664+ permission = 'launchpad.LimitedView'
665+ usedfor = ISpecificationView
666+
667+ def checkAuthenticated(self, user):
668+ return self.obj.userCanView(user)
669+
670+ def checkUnauthenticated(self):
671+ return self.obj.userCanView(None)
672+
673+
674+class EditWhiteboardSpecification(ViewSpecification):
675+
676+ permission = 'launchpad.AnyAllowedPerson'
677+ usedfor = ISpecificationView
678+
679+ def checkUnauthenticated(self):
680+ return False
681+
682+
683 class EditSpecificationByRelatedPeople(AuthorizationBase):
684 """We want everybody "related" to a specification to be able to edit it.
685 You are related if you have a role on the spec, or if you have a role on