launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #11135
[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