← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/specification-auth-check into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/specification-auth-check/+merge/120430

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.

-- 
https://code.launchpad.net/~adeuring/launchpad/specification-auth-check/+merge/120430
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/specification-auth-check into lp:launchpad.
=== modified file 'lib/lp/blueprints/browser/tests/test_specificationdependency.py'
--- lib/lp/blueprints/browser/tests/test_specificationdependency.py	2012-01-01 02:58:52 +0000
+++ lib/lp/blueprints/browser/tests/test_specificationdependency.py	2012-08-20 16:41:26 +0000
@@ -9,7 +9,10 @@
 __metaclass__ = type
 
 from lp.services.webapp import canonical_url
-from lp.testing import BrowserTestCase
+from lp.testing import (
+    BrowserTestCase,
+    person_logged_in,
+    )
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
@@ -21,7 +24,14 @@
         # field of the form to add a dependency to a spec.
         spec = self.factory.makeSpecification(owner=self.user)
         dependency = self.factory.makeSpecification()
+        dependency_url = canonical_url(dependency)
         browser = self.getViewBrowser(spec, '+linkdependency')
-        browser.getControl('Depends On').value = canonical_url(dependency)
+        browser.getControl('Depends On').value = dependency_url
         browser.getControl('Continue').click()
-        self.assertIn(dependency, spec.dependencies)
+        # click() above issues a request, and
+        # ZopePublication.endRequest() calls
+        # zope.security.management.endInteraction().
+        # We need a new interaction for the permission checks
+        # on ISpecification objects.
+        with person_logged_in(None):
+            self.assertIn(dependency, spec.dependencies)

=== modified file 'lib/lp/blueprints/configure.zcml'
--- lib/lp/blueprints/configure.zcml	2012-05-17 07:46:56 +0000
+++ lib/lp/blueprints/configure.zcml	2012-08-20 16:41:26 +0000
@@ -149,9 +149,20 @@
 
   <class class="lp.blueprints.model.specification.Specification">
     <allow interface="lp.blueprints.interfaces.specification.ISpecificationPublic"/>
-    <!-- We allow any authenticated person to update the whiteboard -->
-    <require
-        permission="launchpad.AnyPerson"
+    <require
+        permission="launchpad.Edit"
+        set_attributes="information_type" />
+
+    <require
+        permission="launchpad.LimitedView"
+        interface="lp.blueprints.interfaces.specification.ISpecificationView" />
+
+    <!-- For publicly visible specifications, we allow any authenticated
+         person to update the whiteboard. The whiteboard of restricted
+         specification may only be changed by people who may also view
+         the specification. -->
+    <require
+        permission="launchpad.AnyAllowedPerson"
         set_attributes="whiteboard"/>
     <!-- NB: goals and goalstatus are not to be set directly, it should
          only be set through the proposeGoal / acceptBy / declineBy
@@ -166,11 +177,12 @@
     <require
         permission="launchpad.Admin"
         set_attributes="priority direction_approved"/>
-    <allow
+    <require
+        permission="launchpad.LimitedView"
         attributes="bugs
                     bug_links"/>
     <require
-        permission="launchpad.AnyPerson"
+        permission="launchpad.AnyAllowedPerson"
         attributes="linkBug
                     unlinkBug
                     setWorkItems"/>

=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py	2012-05-17 07:46:56 +0000
+++ lib/lp/blueprints/interfaces/specification.py	2012-08-20 16:41:26 +0000
@@ -9,9 +9,10 @@
 
 __all__ = [
     'ISpecification',
+    'ISpecificationDelta',
     'ISpecificationPublic',
     'ISpecificationSet',
-    'ISpecificationDelta',
+    'ISpecificationView',
     ]
 
 
@@ -47,6 +48,7 @@
     )
 
 from lp import _
+from lp.app.interfaces.launchpad import IPrivacy
 from lp.app.validators import LaunchpadValidationError
 from lp.app.validators.url import valid_webref
 from lp.blueprints.enums import (
@@ -70,6 +72,7 @@
 from lp.blueprints.interfaces.sprint import ISprint
 from lp.bugs.interfaces.buglink import IBugLinkTarget
 from lp.code.interfaces.branchlink import IHasLinkedBranches
+from lp.registry.enums import InformationType
 from lp.registry.interfaces.milestone import IMilestone
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.projectgroup import IProjectGroup
@@ -147,11 +150,28 @@
                         specification.title))
 
 
-class ISpecificationPublic(IHasOwner, IHasLinkedBranches):
+class ISpecificationPublic(IPrivacy):
     """Specification's public attributes and methods."""
 
     id = Int(title=_("Database ID"), required=True, readonly=True)
 
+    information_type = exported(
+        Choice(
+            title=_('Information Type'), vocabulary=InformationType,
+            required=True, readonly=True,
+            description=_(
+                'The type of information contained in this bug report.')))
+
+    def userCanView(user):
+        """Return True if `user` can see this ISpecification, false otherwise.
+        """
+
+
+class ISpecificationView(IHasOwner, IHasLinkedBranches):
+    """Specification's attributes and methods that require
+    the permission launchpad.LimitedView.
+    """
+
     name = exported(
         SpecNameField(
             title=_('Name'), required=True, readonly=False,
@@ -550,21 +570,21 @@
     """Specification's attributes and methods protected with launchpad.Edit.
     """
 
-    @mutator_for(ISpecificationPublic['definition_status'])
+    @mutator_for(ISpecificationView['definition_status'])
     @call_with(user=REQUEST_USER)
     @operation_parameters(
         definition_status=copy_field(
-            ISpecificationPublic['definition_status']))
+            ISpecificationView['definition_status']))
     @export_write_operation()
     @operation_for_version("devel")
     def setDefinitionStatus(definition_status, user):
         """Mutator for definition_status that calls updateLifeCycle."""
 
-    @mutator_for(ISpecificationPublic['implementation_status'])
+    @mutator_for(ISpecificationView['implementation_status'])
     @call_with(user=REQUEST_USER)
     @operation_parameters(
         implementation_status=copy_field(
-            ISpecificationPublic['implementation_status']))
+            ISpecificationView['implementation_status']))
     @export_write_operation()
     @operation_for_version("devel")
     def setImplementationStatus(implementation_status, user):
@@ -602,13 +622,13 @@
         """
 
 
-class ISpecification(ISpecificationPublic, ISpecificationEditRestricted,
-                     IBugLinkTarget):
+class ISpecification(ISpecificationPublic, ISpecificationView,
+                     ISpecificationEditRestricted, IBugLinkTarget):
     """A Specification."""
 
     export_as_webservice_entry(as_of="beta")
 
-    @mutator_for(ISpecificationPublic['workitems_text'])
+    @mutator_for(ISpecificationView['workitems_text'])
     @operation_parameters(new_work_items=WorkItemsText())
     @export_write_operation()
     @operation_for_version('devel')

=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2012-08-07 02:31:56 +0000
+++ lib/lp/blueprints/model/specification.py	2012-08-20 16:41:26 +0000
@@ -66,6 +66,11 @@
 from lp.bugs.interfaces.bugtaskfilter import filter_bugtasks_by_context
 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
 from lp.bugs.model.buglinktarget import BugLinkTargetMixin
+from lp.registry.enums import (
+    InformationType,
+    PRIVATE_INFORMATION_TYPES,
+    PUBLIC_INFORMATION_TYPES,
+    )
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.person import validate_public_person
@@ -210,6 +215,8 @@
     blocked_specs = SQLRelatedJoin('Specification', joinColumn='dependency',
         otherColumn='specification', orderBy='title',
         intermediateTable='SpecificationDependency')
+    information_type = EnumCol(
+        enum=InformationType, notNull=True, default=InformationType.PUBLIC)
 
     @cachedproperty
     def subscriptions(self):
@@ -809,6 +816,26 @@
         return '<Specification %s %r for %r>' % (
             self.id, self.name, self.target.name)
 
+    @property
+    def private(self):
+        return self.information_type in PRIVATE_INFORMATION_TYPES
+
+    def userCanView(self, user):
+        """See `ISpecification`."""
+        if self.information_type in PUBLIC_INFORMATION_TYPES:
+            return True
+        if user is None:
+            return False
+        # Temporary: we should access the grant tables instead of
+        # checking if a given user has special roles.
+        # The following is basically copied from
+        # EditSpecificationByRelatedPeople.checkAuthenticated()
+        return (user.in_admin or
+                user.isOwner(self.target) or
+                user.isOneOfDrivers(self.target) or
+                user.isOneOf(
+                    self, ['owner', 'drafter', 'assignee', 'approver']))
+
 
 class HasSpecificationsMixin:
     """A mixin class that implements many of the common shortcut properties

=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py	2012-02-29 13:03:19 +0000
+++ lib/lp/blueprints/tests/test_specification.py	2012-08-20 16:41:26 +0000
@@ -6,10 +6,18 @@
 __metaclass__ = type
 
 
-from zope.component import getUtility
+from zope.component import (
+    getUtility,
+    queryAdapter,
+    )
+from zope.security.checker import (
+    CheckerPublic,
+    getChecker,
+    )
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
+from lp.app.interfaces.security import IAuthorization
 from lp.blueprints.enums import (
     NewSpecificationDefinitionStatus,
     SpecificationDefinitionStatus,
@@ -17,12 +25,24 @@
     )
 from lp.blueprints.errors import TargetAlreadyHasSpecification
 from lp.blueprints.interfaces.specification import ISpecificationSet
+from lp.registry.enums import (
+    PRIVATE_INFORMATION_TYPES,
+    PUBLIC_INFORMATION_TYPES,
+    )
 from lp.services.webapp.authorization import check_permission
+from lp.security import (
+    AdminSpecification,
+    EditSpecificationByRelatedPeople,
+    EditWhiteboardSpecification,
+    ViewSpecification,
+    )
 from lp.testing import (
     login_person,
+    person_logged_in,
     TestCaseWithFactory,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.services.webapp.interaction import ANONYMOUS
 
 
 class SpecificationTests(TestCaseWithFactory):
@@ -107,6 +127,220 @@
         self.assertRaises(
             Unauthorized, getattr, specification, 'setTarget')
 
+    def check_permissions(self, expected_permissions, used_permissions,
+                             type_):
+        expected = set(expected_permissions.keys())
+        self.assertEqual(
+            expected, set(used_permissions.values()),
+            'Unexpected %s permissions' % type_)
+        for permission in expected_permissions:
+            attribute_names = set(
+                name for name, value in used_permissions.items()
+                if value == permission)
+            self.assertEqual(
+                expected_permissions[permission], attribute_names,
+                'Unexpected set of attributes with %s permission %s:\n'
+                'Defined but not expected: %s\n'
+                'Expected but not defined: %s'
+                % (
+                    type_, permission,
+                    attribute_names - expected_permissions[permission],
+                    expected_permissions[permission] - attribute_names))
+
+    def test_get_permissions(self):
+        expected_get_permissions = {
+            CheckerPublic: set((
+                'id', 'information_type', 'private', 'userCanView')),
+            'launchpad.LimitedView': set((
+                'acceptBy', 'all_blocked', 'all_deps', 'approver',
+                'approverID', 'assignee', 'assigneeID', 'blocked_specs',
+                'bug_links', 'bugs', 'completer', 'createDependency',
+                'date_completed', 'date_goal_decided', 'date_goal_proposed',
+                'date_started', 'datecreated', 'declineBy',
+                'definition_status', 'dependencies', 'direction_approved',
+                'distribution', 'distroseries', 'drafter', 'drafterID',
+                'getBranchLink', 'getDelta', 'getLinkedBugTasks',
+                'getSprintSpecification', 'getSubscriptionByName', 'goal',
+                'goal_decider', 'goal_proposer', 'goalstatus',
+                'has_accepted_goal', 'implementation_status', 'informational',
+                'isSubscribed', 'is_blocked', 'is_complete', 'is_incomplete',
+                'is_started', 'lifecycle_status', 'linkBranch', 'linkSprint',
+                'linked_branches', 'man_days', 'milestone', 'name',
+                'notificationRecipientAddresses', 'owner', 'priority',
+                'product', 'productseries', 'proposeGoal', 'removeDependency',
+                'specurl', 'sprint_links', 'sprints', 'starter', 'subscribe',
+                'subscribers', 'subscription', 'subscriptions', 'summary',
+                'superseded_by', 'target', 'title', 'unlinkBranch',
+                'unlinkSprint', 'unsubscribe', 'updateLifecycleStatus',
+                'validateMove', 'whiteboard', 'work_items', 'workitems_text')),
+            'launchpad.Edit': set((
+                'newWorkItem', 'retarget', 'setDefinitionStatus',
+                'setImplementationStatus', 'setTarget', 'updateWorkItems')),
+            'launchpad.AnyAllowedPerson': set((
+                'unlinkBug', 'linkBug', 'setWorkItems')),
+            }
+        specification = self.factory.makeSpecification()
+        checker = getChecker(specification)
+        self.check_permissions(
+            expected_get_permissions, checker.get_permissions, 'get')
+
+    def test_set_permissions(self):
+        expected_get_permissions = {
+            'launchpad.Admin': set(('direction_approved', 'priority')),
+            'launchpad.AnyAllowedPerson': set(('whiteboard', )),
+            'launchpad.Edit': set((
+                'approver', 'assignee', 'definition_status', 'distribution',
+                'drafter', 'implementation_status', 'information_type',
+                'man_days', 'milestone', 'name', 'product', 'specurl',
+                'summary', 'superseded_by', 'title')),
+            }
+        specification = self.factory.makeSpecification()
+        checker = getChecker(specification)
+        self.check_permissions(
+            expected_get_permissions, checker.set_permissions, 'set')
+
+    def test_security_adapters(self):
+        expected_adapters = {
+            CheckerPublic: None,
+            'launchpad.Admin': AdminSpecification,
+            'launchpad.AnyAllowedPerson': EditWhiteboardSpecification,
+            'launchpad.Edit': EditSpecificationByRelatedPeople,
+            'launchpad.LimitedView': ViewSpecification,
+            }
+        specification = self.factory.makeSpecification()
+        for permission in expected_adapters:
+            adapter = queryAdapter(specification, IAuthorization, permission)
+            expected_class = expected_adapters[permission]
+            if expected_class is None:
+                self.assertIsNone(
+                    adapter, 'No security adapter for %s' % permission)
+            else:
+                self.assertTrue(
+                    isinstance(adapter, expected_class),
+                    'Invalid adapter for %s: %s' % (permission, adapter))
+
+    def read_access_to_ISpecificationView(self, user, specification,
+                                          error_expected):
+        # Access an attribute whose interface is defined in
+        # ISPecificationView.
+        with person_logged_in(user):
+            if error_expected:
+                self.assertRaises(
+                    Unauthorized, getattr, specification, 'name')
+            else:
+                # Just try to access an attribute. No execption should be
+                # raised.
+                specification.name
+
+    def write_access_to_ISpecificationView(self, user, specification,
+                                           error_expected, attribute, value):
+        # Access an attribute whose interface is defined in
+        # ISPecificationView.
+        with person_logged_in(user):
+            if error_expected:
+                self.assertRaises(
+                    Unauthorized, setattr, specification, attribute, value)
+            else:
+                # Just try to change an attribute. No execption should be
+                # raised.
+                setattr(specification, attribute, value)
+
+    def test_anon_read_access(self):
+        # Anonymous users have access to public specifications...
+        specification = self.factory.makeSpecification()
+        for information_type in PUBLIC_INFORMATION_TYPES:
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.read_access_to_ISpecificationView(
+                ANONYMOUS, specification, error_expected=False)
+        # ...but not to private specifications.
+        for information_type in PRIVATE_INFORMATION_TYPES:
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.read_access_to_ISpecificationView(
+                ANONYMOUS, specification, error_expected=True)
+
+    def test_anon_write_access(self):
+        # Anonymous users do not have write access to specifications.
+        specification = self.factory.makeSpecification()
+        for information_type in (PUBLIC_INFORMATION_TYPES +
+                                 PRIVATE_INFORMATION_TYPES):
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.write_access_to_ISpecificationView(
+                ANONYMOUS, specification, error_expected=True,
+                attribute='whiteboard', value='foo')
+            self.write_access_to_ISpecificationView(
+                ANONYMOUS, specification, error_expected=True,
+                attribute='name', value='foo')
+
+    def test_ordinary_user_read_access(self):
+        # Oridnary users have access to public specifications...
+        specification = self.factory.makeSpecification()
+        user = self.factory.makePerson()
+        for information_type in PUBLIC_INFORMATION_TYPES:
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.read_access_to_ISpecificationView(
+                user, specification, error_expected=False)
+        # ...but not to private specifications.
+        for information_type in PRIVATE_INFORMATION_TYPES:
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.read_access_to_ISpecificationView(
+                user, specification, error_expected=True)
+
+    def test_ordinary_user_write_access(self):
+        # Oridnary users can change the whiteborad of public specifications.
+        # They cannot change other attributes.
+        specification = self.factory.makeSpecification()
+        user = self.factory.makePerson()
+        for information_type in PUBLIC_INFORMATION_TYPES:
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.write_access_to_ISpecificationView(
+                user, specification, error_expected=False,
+                attribute='whiteboard', value='foo')
+            self.write_access_to_ISpecificationView(
+                user, specification, error_expected=True,
+                attribute='name', value='foo')
+        # The cannot change any attribute of private specifcations.
+        for information_type in PRIVATE_INFORMATION_TYPES:
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.write_access_to_ISpecificationView(
+                user, specification, error_expected=True,
+                attribute='whiteboard', value='foo')
+            self.write_access_to_ISpecificationView(
+                user, specification, error_expected=True,
+                attribute='name', value='foo')
+
+    def test_special_user_read_access(self):
+        # Users with special privileges can aceess the attributes
+        # of public and private specifcations.
+        specification = self.factory.makeSpecification()
+        for information_type in (PUBLIC_INFORMATION_TYPES +
+                                 PRIVATE_INFORMATION_TYPES):
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.read_access_to_ISpecificationView(
+                specification.owner, specification, error_expected=False)
+
+    def test_special_user_write_access(self):
+        # Users with special privileges can change the attributes
+        # of public and private specifcations.
+        specification = self.factory.makeSpecification()
+        for information_type in (PUBLIC_INFORMATION_TYPES +
+                                 PRIVATE_INFORMATION_TYPES):
+            with person_logged_in(specification.owner):
+                specification.information_type = information_type
+            self.write_access_to_ISpecificationView(
+                specification.owner, specification, error_expected=False,
+                attribute='whiteboard', value='foo')
+            self.write_access_to_ISpecificationView(
+                specification.owner, specification, error_expected=False,
+                attribute='name', value='foo')
+
 
 class TestSpecificationSet(TestCaseWithFactory):
 

=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py	2012-04-16 03:38:00 +0000
+++ lib/lp/blueprints/tests/test_webservice.py	2012-08-20 16:41:26 +0000
@@ -34,8 +34,12 @@
 
     def getSpecOnWebservice(self, spec_object):
         launchpadlib = self.getLaunchpadlib()
-        return launchpadlib.load(
-            '/%s/+spec/%s' % (spec_object.target.name, spec_object.name))
+        # Ensure that there is an interaction so that the security
+        # checks for spec_object work.
+        with person_logged_in(ANONYMOUS):
+            url = '/%s/+spec/%s' % (spec_object.target.name, spec_object.name)
+        result = launchpadlib.load(url)
+        return result
 
     def getPillarOnWebservice(self, pillar_obj):
         launchpadlib = self.getLaunchpadlib()
@@ -53,52 +57,59 @@
         # it's ready for prime time.
         spec = self.factory.makeSpecification()
         user = self.factory.makePerson()
+        url = '/%s/+spec/%s' % (spec.product.name, spec.name)
         webservice = webservice_for_person(user)
-        response = webservice.get(
-            '/%s/+spec/%s' % (spec.product.name, spec.name))
+        response = webservice.get(url)
         expected_keys = [u'self_link', u'http_etag', u'resource_type_link',
-                         u'web_link']
+                         u'web_link', u'information_type']
         self.assertEqual(response.status, 200)
         self.assertContentEqual(expected_keys, response.jsonBody().keys())
 
     def test_representation_contains_name(self):
         spec = self.factory.makeSpecification()
+        spec_name = spec.name
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.name, spec_webservice.name)
+        self.assertEqual(spec_name, spec_webservice.name)
 
     def test_representation_contains_target(self):
         spec = self.factory.makeSpecification(
             product=self.factory.makeProduct())
+        spec_target = spec.target
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.target.name, spec_webservice.target.name)
+        self.assertEqual(spec_target.name, spec_webservice.target.name)
 
     def test_representation_contains_title(self):
         spec = self.factory.makeSpecification(title='Foo')
+        spec_title = spec.title
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.title, spec_webservice.title)
+        self.assertEqual(spec_title, spec_webservice.title)
 
     def test_representation_contains_specification_url(self):
         spec = self.factory.makeSpecification(specurl='http://example.com')
+        spec_specurl = spec.specurl
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.specurl, spec_webservice.specification_url)
+        self.assertEqual(spec_specurl, spec_webservice.specification_url)
 
     def test_representation_contains_summary(self):
         spec = self.factory.makeSpecification(summary='Foo')
+        spec_summary = spec.summary
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.summary, spec_webservice.summary)
+        self.assertEqual(spec_summary, spec_webservice.summary)
 
     def test_representation_contains_implementation_status(self):
         spec = self.factory.makeSpecification()
+        spec_implementation_status = spec.implementation_status
         spec_webservice = self.getSpecOnWebservice(spec)
         self.assertEqual(
-            spec.implementation_status.title,
+            spec_implementation_status.title,
             spec_webservice.implementation_status)
 
     def test_representation_contains_definition_status(self):
         spec = self.factory.makeSpecification()
+        spec_definition_status = spec.definition_status
         spec_webservice = self.getSpecOnWebservice(spec)
         self.assertEqual(
-            spec.definition_status.title, spec_webservice.definition_status)
+            spec_definition_status.title, spec_webservice.definition_status)
 
     def test_representation_contains_assignee(self):
         # Hard-code the person's name or else we'd need to set up a zope
@@ -128,18 +139,21 @@
 
     def test_representation_contains_priority(self):
         spec = self.factory.makeSpecification()
+        spec_priority = spec.priority
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.priority.title, spec_webservice.priority)
+        self.assertEqual(spec_priority.title, spec_webservice.priority)
 
     def test_representation_contains_date_created(self):
         spec = self.factory.makeSpecification()
+        spec_datecreated = spec.datecreated
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.datecreated, spec_webservice.date_created)
+        self.assertEqual(spec_datecreated, spec_webservice.date_created)
 
     def test_representation_contains_whiteboard(self):
         spec = self.factory.makeSpecification(whiteboard='Test')
+        spec_whiteboard = spec.whiteboard
         spec_webservice = self.getSpecOnWebservice(spec)
-        self.assertEqual(spec.whiteboard, spec_webservice.whiteboard)
+        self.assertEqual(spec_whiteboard, spec_webservice.whiteboard)
 
     def test_representation_contains_workitems(self):
         work_item = self.factory.makeSpecificationWorkItem()
@@ -160,10 +174,11 @@
     def test_representation_contains_dependencies(self):
         spec = self.factory.makeSpecification()
         spec2 = self.factory.makeSpecification()
+        spec2_name = spec2.name
         spec.createDependency(spec2)
         spec_webservice = self.getSpecOnWebservice(spec)
         self.assertEqual(1, spec_webservice.dependencies.total_size)
-        self.assertEqual(spec2.name, spec_webservice.dependencies[0].name)
+        self.assertEqual(spec2_name, spec_webservice.dependencies[0].name)
 
     def test_representation_contains_linked_branches(self):
         spec = self.factory.makeSpecification()

=== modified file 'lib/lp/permissions.zcml'
--- lib/lp/permissions.zcml	2011-12-24 16:28:37 +0000
+++ lib/lp/permissions.zcml	2012-08-20 16:41:26 +0000
@@ -20,6 +20,11 @@
     access_level="write" />
 
   <permission
+    id="launchpad.AnyAllowedPerson"
+    title="Any Authenticated Person for public data; any person having grants on restricted objects."
+    access_level="write" />
+
+  <permission
     id="launchpad.Edit" title="Editing something" access_level="write" />
 
   <!-- Request large downloads, or other heavyweight jobs that are not

=== modified file 'lib/lp/security.py'
--- lib/lp/security.py	2012-08-16 13:27:16 +0000
+++ lib/lp/security.py	2012-08-20 16:41:26 +0000
@@ -45,6 +45,7 @@
 from lp.blueprints.interfaces.specification import (
     ISpecification,
     ISpecificationPublic,
+    ISpecificationView,
     )
 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
 from lp.blueprints.interfaces.specificationsubscription import (
@@ -519,6 +520,27 @@
     usedfor = ISpecificationPublic
 
 
+class ViewSpecification(AuthorizationBase):
+
+    permission = 'launchpad.LimitedView'
+    usedfor = ISpecificationView
+
+    def checkAuthenticated(self, user):
+        return self.obj.userCanView(user)
+
+    def checkUnauthenticated(self):
+        return self.obj.userCanView(None)
+
+
+class EditWhiteboardSpecification(ViewSpecification):
+
+    permission = 'launchpad.AnyAllowedPerson'
+    usedfor = ISpecificationView
+
+    def checkUnauthenticated(self):
+        return False
+
+
 class EditSpecificationByRelatedPeople(AuthorizationBase):
     """We want everybody "related" to a specification to be able to edit it.
     You are related if you have a role on the spec, or if you have a role on


Follow ups