launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13437
[Merge] lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad
Abel Deuring has proposed merging lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~adeuring/launchpad/milestone-sec-adapter/+merge/129907
This branch adds a "sharing wawre" security adapter for IMilestone.
Milestones related to private products are now only shown to persons
with policy grants for the product; the only exception are some
attributes that leak no information: the database ID, and the methods
userCanView(), checkAuthenticated(), checkUnauthenticated().
The two latter methods should not appear at all -- we use adapters
for the security checks, so there is no need to define them in the
main class. And in fact they are not implemented at all.
Similary, IMilestone and some other interface classes included in
IMilestone define a few attributes that not implemented by class
Milestone.
We do not have very much time to finish the "Private products" story,
so I simply tried to ignore these inconsistencies.
Details of the change:
lp/registry/configure.zcml:
Access to most attributes and to "partial" interfaces that were public
now requires the permission launchpad.View; the permission
launchpad.AnyPerson is replaced with launchapd.AnyAllowedPerson.
(lp.services.webapp.authorization.LaunchpadSecurityPolicy.checkPermission()
has a "shortcut" for the permission launchpad.AnyPerson: no
dedicated security adapters are looked up for this permission,
so the new rule "data for milestones of private product should only
be visible for person having a policy grant" cannot be implemented
with this permission.)
I also sorted the attributes requiring lp.View alphabetically.
lp/security.py:
two new adapters for IMilestone and the permissions lp.View and
lp.AnyAllowedPerson, respectively.
Both methods delegate the security check to the new method
Milestone.userCanView()
I akso removed some unnecessary imports.
lp/registry/model/milestone.py:
The new method userCanView(). The acutal permission check is done
by IProduct.userCanView()
lp/registry/tests/test_milestone.py:
Tests for the permissions.
The test class properties expected_get_permissions and
expected_set_permissions are also intended to document which permissions
are acutally used for IMilestone.
The tests are similar to the test pattern I also used for the
security adapters of IProduct and ISpecification, so I moved the
common method checkPermissions() to lp.testing.TestCase.
test:
./bin/test registry -vvt lp.registry.tests.test_milestone.MilestoneSecurityAdaperTestCase
no lint
--
https://code.launchpad.net/~adeuring/launchpad/milestone-sec-adapter/+merge/129907
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad.
=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py 2012-09-24 13:43:00 +0000
+++ lib/lp/blueprints/tests/test_specification.py 2012-10-16 14:36:28 +0000
@@ -139,26 +139,6 @@
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((
@@ -196,7 +176,7 @@
}
specification = self.factory.makeSpecification()
checker = getChecker(specification)
- self.check_permissions(
+ self.checkPermissions(
expected_get_permissions, checker.get_permissions, 'get')
def test_set_permissions(self):
@@ -211,7 +191,7 @@
}
specification = self.factory.makeSpecification()
checker = getChecker(specification)
- self.check_permissions(
+ self.checkPermissions(
expected_get_permissions, checker.set_permissions, 'set')
def test_security_adapters(self):
=== modified file 'lib/lp/bugs/browser/bugtask.py'
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-15 09:15:29 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-16 14:36:28 +0000
@@ -157,7 +157,11 @@
def test_rendered_query_counts_constant_with_attachments(self):
with celebrity_logged_in('admin'):
browses_under_limit = BrowsesWithQueryLimit(
+<<<<<<< TREE
85, self.factory.makePerson())
+=======
+ 95, self.factory.makePerson())
+>>>>>>> MERGE-SOURCE
# First test with a single attachment.
task = self.factory.makeBugTask()
@@ -240,6 +244,7 @@
[activity] = view.interesting_activity
self.assertEqual("description", activity.whatchanged)
+<<<<<<< TREE
def test_rendered_query_counts_constant_with_activities(self):
# More queries are not used for extra bug activities.
task = self.factory.makeBugTask()
@@ -286,6 +291,33 @@
self.assertThat(bug, browses_under_limit)
+=======
+ def test_rendered_query_counts_constant_with_activities(self):
+ # More queries are not used for extra bug activities.
+ task = self.factory.makeBugTask()
+
+ def add_activity(what, who):
+ getUtility(IBugActivitySet).new(
+ task.bug, datetime.now(UTC), who, whatchanged=what)
+
+ # Render the view with one activity.
+ with celebrity_logged_in('admin'):
+ browses_under_limit = BrowsesWithQueryLimit(
+ 93, self.factory.makePerson())
+ person = self.factory.makePerson()
+ add_activity("description", person)
+
+ self.assertThat(task, browses_under_limit)
+
+ # Render the view with many more activities by different people.
+ with celebrity_logged_in('admin'):
+ for _ in range(20):
+ person = self.factory.makePerson()
+ add_activity("description", person)
+
+ self.assertThat(task, browses_under_limit)
+
+>>>>>>> MERGE-SOURCE
def test_error_for_changing_target_with_invalid_status(self):
# If a user moves a bug task with a restricted status (say,
# Triaged) to a target where they do not have permission to set
=== modified file 'lib/lp/bugs/model/bug.py'
=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
--- lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-12 14:53:10 +0000
+++ lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-16 14:36:28 +0000
@@ -400,6 +400,40 @@
layer = DatabaseFunctionalLayer
+<<<<<<< TREE
+=======
+ def test_default_private_bugs_true(self):
+ # Verify the path through user submission, to MaloneApplication to
+ # BugSet, and back to the user creates a private bug according
+ # to the project's bugs are private by default rule.
+ project = self.factory.makeLegacyProduct(
+ licenses=[License.OTHER_PROPRIETARY])
+ target_url = api_url(project)
+ with person_logged_in(project.owner):
+ project.setPrivateBugs(True, project.owner)
+ webservice = launchpadlib_for('test', 'salgado')
+ bugs_collection = webservice.load('/bugs')
+ bug = bugs_collection.createBug(
+ target=target_url, title='title', description='desc')
+ self.assertEqual('Private', bug.information_type)
+
+ def test_explicit_private_private_bugs_true(self):
+ # Verify the path through user submission, to MaloneApplication to
+ # BugSet, and back to the user creates a private bug because the
+ # user commands it.
+ project = self.factory.makeLegacyProduct(
+ licenses=[License.OTHER_PROPRIETARY])
+ target_url = api_url(project)
+ with person_logged_in(project.owner):
+ project.setPrivateBugs(True, project.owner)
+ webservice = launchpadlib_for('test', 'salgado')
+ bugs_collection = webservice.load('/bugs')
+ bug = bugs_collection.createBug(
+ target=target_url, title='title', description='desc',
+ private=True)
+ self.assertEqual('Private', bug.information_type)
+
+>>>>>>> MERGE-SOURCE
def test_default_sharing_policy_proprietary(self):
# Verify the path through user submission, to MaloneApplication to
# BugSet, and back to the user creates a private bug according
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
=== modified file 'lib/lp/code/browser/tests/test_product.py'
=== modified file 'lib/lp/code/model/branchnamespace.py'
--- lib/lp/code/model/branchnamespace.py 2012-10-11 08:09:12 +0000
+++ lib/lp/code/model/branchnamespace.py 2012-10-16 14:36:28 +0000
@@ -369,6 +369,7 @@
def getAllowedInformationTypes(self, who=None):
"""See `IBranchNamespace`."""
+<<<<<<< TREE
# The project uses the new simplified branch_sharing_policy
# rules, so check them.
@@ -387,6 +388,60 @@
return BRANCH_POLICY_ALLOWED_TYPES[
self.product.branch_sharing_policy]
+=======
+ if not self._using_branchvisibilitypolicy:
+ # The project uses the new simplified branch_sharing_policy
+ # rules, so check them.
+
+ # Some policies require that the branch owner or current user have
+ # full access to an information type. If it's required and the user
+ # doesn't hold it, no information types are legal.
+ required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
+ self.product.branch_sharing_policy]
+ if (required_grant is not None
+ and not getUtility(IService, 'sharing').checkPillarAccess(
+ [self.product], required_grant, self.owner)
+ and (who is None
+ or not getUtility(IService, 'sharing').checkPillarAccess(
+ [self.product], required_grant, who))):
+ return []
+
+ return BRANCH_POLICY_ALLOWED_TYPES[
+ self.product.branch_sharing_policy]
+
+ # The project still uses BranchVisibilityPolicy, so check that.
+ private_rules = (
+ BranchVisibilityRule.PRIVATE,
+ BranchVisibilityRule.PRIVATE_ONLY)
+
+ rule = self.product.getBranchVisibilityRuleForTeam(self.owner)
+ if rule is not None:
+ # If there is an explicit rule for the namespace owner, use that.
+ private = rule in private_rules
+ public = rule != BranchVisibilityRule.PRIVATE_ONLY
+ else:
+ # Otherwise find all the rules for the owner's teams.
+ related_rules = set(p.rule for p in self._getRelatedPolicies())
+
+ # If any of the rules allow private branches, allow them.
+ private = bool(related_rules.intersection(private_rules))
+
+ # If any of the rules allow public branches, allow them.
+ if related_rules.difference([BranchVisibilityRule.PRIVATE_ONLY]):
+ public = True
+ else:
+ # There's no team-specific rules, or none of them allow
+ # public branches. Fall back to the default rule.
+ base_rule = self.product.getBaseBranchVisibilityRule()
+ public = base_rule == BranchVisibilityRule.PUBLIC
+
+ types = []
+ if public:
+ types.extend(PUBLIC_INFORMATION_TYPES)
+ if private:
+ types.extend(FREE_PRIVATE_INFORMATION_TYPES)
+ return types
+>>>>>>> MERGE-SOURCE
def getDefaultInformationType(self, who=None):
"""See `IBranchNamespace`."""
=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
=== modified file 'lib/lp/registry/browser/configure.zcml'
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-10-12 19:34:12 +0000
+++ lib/lp/registry/browser/product.py 2012-10-16 14:36:28 +0000
@@ -1392,6 +1392,7 @@
]
custom_widget('licenses', LicenseWidget)
custom_widget('license_info', GhostWidget)
+<<<<<<< TREE
custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
@property
@@ -1405,6 +1406,19 @@
return canonical_url(getUtility(IProductSet))
cancel_url = next_url
+=======
+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
+
+ @property
+ def next_url(self):
+ """See `LaunchpadFormView`."""
+ if self.context.active:
+ return canonical_url(self.context)
+ else:
+ return canonical_url(getUtility(IProductSet))
+
+ cancel_url = next_url
+>>>>>>> MERGE-SOURCE
@property
def page_title(self):
@@ -1441,11 +1455,16 @@
@action("Change", name='change')
def change_action(self, action, data):
+<<<<<<< TREE
try:
self.updateContextFromData(data)
except CannotChangeInformationType as e:
self.setFieldError('information_type', str(e))
+=======
+ self.updateContextFromData(data)
+
+>>>>>>> MERGE-SOURCE
class ProductValidationMixin:
=== modified file 'lib/lp/registry/browser/productseries.py'
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2012-10-15 09:15:29 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2012-10-16 14:36:28 +0000
@@ -432,6 +432,7 @@
cache.objects['team_membership_policy_data'])
+<<<<<<< TREE
class TestProductEditView(BrowserTestCase):
"""Tests for the ProductEditView"""
@@ -517,6 +518,76 @@
InformationType.PUBLIC, updated_product.information_type)
+=======
+class TestProductEditView(TestCaseWithFactory):
+ """Tests for the ProductEditView"""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestProductEditView, self).setUp()
+
+ def _make_product_edit_form(self, product, proprietary=False):
+ """Return form data for product edit.
+
+ :param product: Factory product to base the form data base of.
+ """
+ if proprietary:
+ licenses = License.OTHER_PROPRIETARY.title
+ license_info = 'Commercial Subscription'
+ information_type = 'PROPRIETARY'
+ else:
+ licenses = ['MIT']
+ license_info = ''
+ information_type = 'PUBLIC'
+
+ return {
+ 'field.actions.change': 'Change',
+ 'field.name': product.name,
+ 'field.displayname': product.displayname,
+ 'field.title': product.title,
+ 'field.summary': product.summary,
+ 'field.information_type': information_type,
+ 'field.licenses': licenses,
+ 'field.license_info': license_info,
+ }
+
+ def test_change_information_type_proprietary(self):
+ product = self.factory.makeProduct(name='fnord')
+ with FeatureFixture({u'disclosure.private_projects.enabled': u'on'}):
+ login_person(product.owner)
+ form = self._make_product_edit_form(product, proprietary=True)
+ view = create_initialized_view(product, '+edit', form=form)
+ self.assertEqual(0, len(view.errors))
+
+ product_set = getUtility(IProductSet)
+ updated_product = product_set.getByName('fnord')
+ self.assertEqual(
+ InformationType.PROPRIETARY, updated_product.information_type)
+ # A complimentary commercial subscription is auto generated for
+ # the product when the information type is changed.
+ self.assertIsNotNone(updated_product.commercial_subscription)
+
+ def test_change_information_type_public(self):
+ owner = self.factory.makePerson(name='pting')
+ product = self.factory.makeProduct(
+ name='fnord',
+ information_type=InformationType.PROPRIETARY,
+ owner=owner,
+ )
+ with FeatureFixture({u'disclosure.private_projects.enabled': u'on'}):
+ login_person(owner)
+ form = self._make_product_edit_form(product)
+ view = create_initialized_view(product, '+edit', form=form)
+ self.assertEqual(0, len(view.errors))
+
+ product_set = getUtility(IProductSet)
+ updated_product = product_set.getByName('fnord')
+ self.assertEqual(
+ InformationType.PUBLIC, updated_product.information_type)
+
+
+>>>>>>> MERGE-SOURCE
class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory):
"""Tests the ProductSetReviewLicensesView."""
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/configure.zcml 2012-10-16 14:36:28 +0000
@@ -1026,22 +1026,30 @@
<allow
attributes="
id
- name
+ userCanView
+ " />
+ <require
+ permission="launchpad.View"
+ attributes="
+ active
+ bugtasks
code_name
+ dateexpected
+ displayname
+ distribution
+ distroseries
+ getTags
+ getTagsData
+ name
product
- distribution
+ product_release
productseries
- distroseries
- dateexpected
- active
- summary
- target
series_target
- displayname
- title
- bugtasks
specifications
- product_release"/>
+ summary
+ target
+ title
+ "/>
<require
permission="launchpad.Edit"
attributes="
@@ -1051,15 +1059,13 @@
setTags
"/>
<require
- permission="zope.Public"
- attributes="
- getTags
- getTagsData
- "/>
- <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
- <allow
+ permission="launchpad.View"
+ interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
+ <require
+ permission="launchpad.View"
interface="lp.bugs.interfaces.bugtarget.IHasBugs"/>
- <allow
+ <require
+ permission="launchpad.View"
interface="lp.bugs.interfaces.bugtarget.IHasOfficialBugTags"/>
<allow
interface="lp.app.interfaces.security.IAuthorization"/>
@@ -1070,10 +1076,11 @@
<!-- IStructuralSubscriptionTarget -->
- <allow
+ <require
+ permission="launchpad.View"
interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
<require
- permission="launchpad.AnyPerson"
+ permission="launchpad.AnyAllowedPerson"
interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
</class>
@@ -1229,6 +1236,7 @@
class="lp.registry.model.product.Product">
<allow
interface="lp.registry.interfaces.product.IProductPublic"/>
+<<<<<<< TREE
<allow
interface="lp.registry.interfaces.pillar.IPillar"/>
<require
@@ -1239,6 +1247,19 @@
interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
<require
permission="launchpad.View"
+=======
+ <require
+ permission="launchpad.View"
+ interface="lp.registry.interfaces.product.IPillar"/>
+ <require
+ permission="launchpad.View"
+ interface="lp.registry.interfaces.product.IProductLimitedView"/>
+ <require
+ permission="launchpad.View"
+ interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
+ <require
+ permission="launchpad.View"
+>>>>>>> MERGE-SOURCE
interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
<require
permission="launchpad.AnyAllowedPerson"
@@ -1371,8 +1392,14 @@
<!-- IStructuralSubscriptionTarget -->
+<<<<<<< TREE
<require
permission="launchpad.View"
+=======
+ <!-- XXX bug=1065162 Abel Deuring 2012-10-10:
+ This interface should require the permission launchpad.View. -->
+ <allow
+>>>>>>> MERGE-SOURCE
interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
<require
permission="launchpad.AnyAllowedPerson"
=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py 2012-10-15 02:32:30 +0000
+++ lib/lp/registry/interfaces/milestone.py 2012-10-16 14:36:28 +0000
@@ -267,6 +267,9 @@
why this is not a property.
"""
+ def userCanView(user):
+ """True if the given user has access to this product."""
+
# Avoid circular imports
IBugTask['milestone'].schema = IMilestone
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2012-10-12 14:53:10 +0000
+++ lib/lp/registry/interfaces/product.py 2012-10-16 14:36:28 +0000
@@ -420,15 +420,45 @@
"Not applicable to 'Other/Proprietary'.")))
-class IProductPublic(Interface):
-
- id = Int(title=_('The Project ID'))
-
- def userCanView(user):
- """True if the given user has access to this product."""
-
-
-class IProductLimitedView(
+<<<<<<< TREE
+class IProductPublic(Interface):
+
+ id = Int(title=_('The Project ID'))
+
+ def userCanView(user):
+ """True if the given user has access to this product."""
+
+
+class IProductLimitedView(
+=======
+class IProductPublic(Interface):
+
+ id = Int(title=_('The Project ID'))
+
+ def userCanView(user):
+ """True if the given user has access to this product."""
+
+ # bug=1065162 Abel Deuring 2012-10-10:
+ # name and displayname should be defined in IProductLimitedView
+ name = exported(
+ ProductNameField(
+ title=_('Name'),
+ constraint=name_validator,
+ description=_(
+ "At least one lowercase letter or number, followed by "
+ "letters, numbers, dots, hyphens or pluses. "
+ "Keep this name short; it is used in URLs as shown above.")))
+
+ displayname = exported(
+ TextLine(
+ title=_('Display Name'),
+ description=_("""The name of the project as it would appear in a
+ paragraph.""")),
+ exported_as='display_name')
+
+
+class IProductLimitedView(
+>>>>>>> MERGE-SOURCE
IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
IHasDrivers, IHasExternalBugTracker, IHasIcon,
IHasLogo, IHasMergeProposals, IHasMilestones, IHasExpirableBugs,
@@ -488,22 +518,6 @@
"required because there might be a project driver and also a "
"driver appointed in the overarching project group.")
- name = exported(
- ProductNameField(
- title=_('Name'),
- constraint=name_validator,
- description=_(
- "At least one lowercase letter or number, followed by "
- "letters, numbers, dots, hyphens or pluses. "
- "Keep this name short; it is used in URLs as shown above.")))
-
- displayname = exported(
- TextLine(
- title=_('Display Name'),
- description=_("""The name of the project as it would appear in a
- paragraph.""")),
- exported_as='display_name')
-
title = exported(
Title(
title=_('Title'),
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2012-10-06 23:14:35 +0000
+++ lib/lp/registry/model/milestone.py 2012-10-16 14:36:28 +0000
@@ -351,6 +351,17 @@
from lp.registry.model.milestonetag import MilestoneTag
return list(self.getTagsData().values(MilestoneTag.tag))
+ def userCanView(self, user):
+ """See `IMilestone`."""
+ # A database constraint ensures that either self.product
+ # or self.distribution is not None.
+ if self.product is None:
+ # Distributions are always public, and so are their
+ # milestones.
+ return True
+ # Delegate the permission check
+ return self.product.userCanView(user)
+
class MilestoneSet:
implements(IMilestoneSet)
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-15 09:15:29 +0000
+++ lib/lp/registry/model/product.py 2012-10-16 14:36:28 +0000
@@ -68,8 +68,12 @@
FREE_INFORMATION_TYPES,
InformationType,
PRIVATE_INFORMATION_TYPES,
+<<<<<<< TREE
PROPRIETARY_INFORMATION_TYPES,
PUBLIC_INFORMATION_TYPES,
+=======
+ PUBLIC_INFORMATION_TYPES,
+>>>>>>> MERGE-SOURCE
PUBLIC_PROPRIETARY_INFORMATION_TYPES,
service_uses_launchpad,
ServiceUsage,
@@ -432,6 +436,7 @@
# Proprietary check works only after creation, because during
# creation, has_commercial_subscription cannot give the right value
# and triggers an inappropriate DB flush.
+<<<<<<< TREE
# If you're changing the license, and setting a PROPRIETARY
# information type, yet you don't have a subscription you get one when
@@ -450,11 +455,33 @@
'A valid commercial subscription is required for private'
' Projects.')
+=======
+
+ # If you're changing the license, and setting a PROPRIETARY
+ # information type, yet you don't have a subscription you get one when
+ # the license is set.
+
+ # If you have a commercial subscription, but it's not current, you
+ # cannot set the information type to a PROPRIETARY type.
+ if not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES:
+ # Create the complimentary commercial subscription for the product.
+ self._ensure_complimentary_subscription()
+
+ if not self.has_current_commercial_subscription:
+ raise CommercialSubscribersOnly(
+ 'A valid commercial subscription is required for private'
+ ' Projects.')
+
+>>>>>>> MERGE-SOURCE
return value
_information_type = EnumCol(
enum=InformationType, default=InformationType.PUBLIC,
+<<<<<<< TREE
dbName="information_type",
+=======
+ dbName='information_type',
+>>>>>>> MERGE-SOURCE
storm_validator=_valid_product_information_type)
def _get_information_type(self):
@@ -1521,6 +1548,7 @@
return weight_function
+<<<<<<< TREE
@cachedproperty
def _known_viewers(self):
"""A set of known persons able to view this product."""
@@ -1554,6 +1582,33 @@
self._known_viewers.add(user.id)
return True
+=======
+ @cachedproperty
+ def _known_viewers(self):
+ """A set of known persons able to view this product."""
+ return set()
+
+ def userCanView(self, user):
+ """See `IProductPublic`."""
+ if self.information_type in PUBLIC_INFORMATION_TYPES:
+ return True
+ if user is None:
+ return False
+ if user.id in self._known_viewers:
+ return True
+ # We want an actual Storm Person.
+ if IPersonRoles.providedBy(user):
+ user = user.person
+ policy = getUtility(IAccessPolicySource).find(
+ [(self, self.information_type)]).one()
+ grants_for_user = getUtility(IAccessPolicyGrantSource).find(
+ [(policy, user)])
+ if grants_for_user.is_empty():
+ return False
+ self._known_viewers.add(user.id)
+ return True
+
+>>>>>>> MERGE-SOURCE
def get_precached_products(products, need_licences=False, need_projects=False,
need_series=False, need_releases=False,
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py 2012-10-02 02:56:32 +0000
+++ lib/lp/registry/tests/test_milestone.py 2012-10-16 14:36:28 +0000
@@ -6,11 +6,20 @@
__metaclass__ = type
from operator import attrgetter
+from storm.exceptions import NoneError
import unittest
from zope.component import getUtility
+from zope.security.checker import (
+ CheckerPublic,
+ getChecker,
+ )
+from zope.security.interfaces import Unauthorized
+from lp.app.enums import InformationType
+from lp.app.interfaces.services import IService
from lp.app.errors import NotFoundError
+from lp.registry.enums import SharingPermission
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.milestone import (
IHasMilestones,
@@ -101,6 +110,244 @@
[1, 2, 3])
+class MilestoneSecurityAdaperTestCase(TestCaseWithFactory):
+ """A TestCase for the security adapter of milestones."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(MilestoneSecurityAdaperTestCase, self).setUp()
+ self.public_product = self.factory.makeProduct()
+ self.public_milestone = self.factory.makeMilestone(
+ product=self.public_product)
+ self.proprietary_product_owner = self.factory.makePerson()
+ self.proprietary_product = self.factory.makeProduct(
+ owner=self.proprietary_product_owner,
+ information_type=InformationType.PROPRIETARY)
+ self.proprietary_milestone = self.factory.makeMilestone(
+ product=self.proprietary_product)
+
+ expected_get_permissions = {
+ CheckerPublic: set((
+ 'id', 'checkAuthenticated', 'checkUnauthenticated',
+ 'userCanView',
+ )),
+ 'launchpad.View': set((
+ 'active', 'bug_subscriptions', 'bugtasks', 'code_name',
+ 'dateexpected', 'displayname', 'distribution', 'distroseries',
+ '_getOfficialTagClause', 'getBugSummaryContextWhereClause',
+ 'getBugTaskWeightFunction', 'getSubscription',
+ 'getSubscriptions', 'getTags', 'getTagsData',
+ 'getUsedBugTagsWithOpenCounts', 'name', 'official_bug_tags',
+ 'parent_subscription_target', 'product', 'product_release',
+ 'productseries', 'searchTasks', 'series_target',
+ 'specifications', 'summary', 'target', 'target_type_display',
+ 'title', 'userCanAlterBugSubscription',
+ 'userCanAlterSubscription', 'userHasBugSubscriptions',
+ )),
+ 'launchpad.AnyAllowedPerson': set((
+ 'addBugSubscription', 'addBugSubscriptionFilter',
+ 'addSubscription', 'removeBugSubscription',
+ )),
+ 'launchpad.Edit': set((
+ 'closeBugsAndBlueprints', 'createProductRelease',
+ 'destroySelf', 'setTags',
+ )),
+ }
+
+ def test_get_permissions(self):
+ milestone = self.factory.makeMilestone()
+ checker = getChecker(milestone)
+ self.checkPermissions(
+ self.expected_get_permissions, checker.get_permissions, 'get')
+
+ expected_set_permissions = {
+ 'launchpad.Edit': set((
+ 'active', 'code_name', 'dateexpected', 'distroseries', 'name',
+ 'product_release', 'productseries', 'summary',
+ )),
+ }
+
+ def test_set_permissions(self):
+ milestone = self.factory.makeMilestone()
+ checker = getChecker(milestone)
+ self.checkPermissions(
+ self.expected_set_permissions, checker.set_permissions, 'set')
+
+ def assertAccessAuthorzized(self, attribute_names, obj):
+ # Try to access the given attributes of obj. No exception
+ # should be raised.
+ for name in attribute_names:
+ # class Milestone does not implenet all attributes defined by
+ # class IMilestone. AttributeErrors caused by attempts to
+ # access these attribues are not relevant here: We simply
+ # want to be sure that no Unauthorized error is raised.
+ try:
+ getattr(obj, name)
+ except AttributeError:
+ pass
+
+ def assertAccessUnauthorzized(self, attribute_names, obj):
+ # Try to access the given attributes of obj. Unauthorized
+ # should be raised.
+ for name in attribute_names:
+ self.assertRaises(Unauthorized, getattr, obj, name)
+
+ def assertChangeAuthorzized(self, attribute_names, obj):
+ # Try to changes the given attributes of obj. Unauthorized
+ # should be raised.
+ for name in attribute_names:
+ # Not all attributes declared in configure.zcml to be
+ # settable actually exist. Attempts to set them raises
+ # an AttributeError. Setting an Attribute to None may no
+ # be allowed.
+ #
+ # Both errors can be ignored here: This method intends only
+ # to prove that Unauthorized is not raised.
+ try:
+ setattr(obj, name, None)
+ except (AttributeError, NoneError):
+ pass
+
+ def assertChangeUnauthorzized(self, attribute_names, obj):
+ # Try to changes the given attributes of obj. Unauthorized
+ # should be raised.
+ for name in attribute_names:
+ self.assertRaises(Unauthorized, setattr, obj, name, None)
+
+ def test_access_for_anonymous(self):
+ # Anonymous users have to public attributes of milestones for
+ # private and public products.
+ with person_logged_in(ANONYMOUS):
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions[CheckerPublic],
+ self.public_milestone)
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions[CheckerPublic],
+ self.proprietary_milestone)
+
+ # They have access to attributes requiring the permission
+ # launchpad.View of milestones for public products...
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions['launchpad.View'],
+ self.public_milestone)
+
+ # ...but not to the same attributes of milestones for private
+ # products.
+ self.assertAccessUnauthorzized(
+ self.expected_get_permissions['launchpad.View'],
+ self.proprietary_milestone)
+
+ # They cannot access other attributes.
+ for permission, names in self.expected_get_permissions.items():
+ if permission in (CheckerPublic, 'launchpad.View'):
+ continue
+ self.assertAccessUnauthorzized(names, self.public_milestone)
+ self.assertAccessUnauthorzized(
+ names, self.proprietary_milestone)
+
+ # They cannot change any attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeUnauthorzized(names, self.public_milestone)
+ self.assertChangeUnauthorzized(
+ names, self.proprietary_milestone)
+
+ def test_access_for_ordinary_user(self):
+ # Regular users have to public attributes of milestones for
+ # private and public products.
+ user = self.factory.makePerson()
+ with person_logged_in(user):
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions[CheckerPublic],
+ self.public_milestone)
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions[CheckerPublic],
+ self.proprietary_milestone)
+
+ # They have access to attributes requiring the permission
+ # launchpad.View or launchpad.AnyAllowedPerson of milestones
+ # for public products...
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions['launchpad.View'],
+ self.public_milestone)
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+ self.public_milestone)
+
+ # ...but not to the same attributes of milestones for private
+ # products.
+ self.assertAccessUnauthorzized(
+ self.expected_get_permissions['launchpad.View'],
+ self.proprietary_milestone)
+ self.assertAccessUnauthorzized(
+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+ self.proprietary_milestone)
+
+ # They cannot access other attributes.
+ for permission, names in self.expected_get_permissions.items():
+ if permission in (
+ CheckerPublic, 'launchpad.View',
+ 'launchpad.AnyAllowedPerson'):
+ continue
+ self.assertAccessUnauthorzized(names, self.public_milestone)
+ self.assertAccessUnauthorzized(
+ names, self.proprietary_milestone)
+
+ # They cannot change attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeUnauthorzized(names, self.public_milestone)
+ self.assertChangeUnauthorzized(
+ names, self.proprietary_milestone)
+
+ def test_access_for_user_with_grant_for_private_product(self):
+ # Users with a policy grant for a private product have access
+ # to most attributes of the private product.
+ user = self.factory.makePerson()
+ with person_logged_in(self.proprietary_product_owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ self.proprietary_product, user, self.proprietary_product_owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+
+ with person_logged_in(user):
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions[CheckerPublic],
+ self.proprietary_milestone)
+
+ # They have access to attributes requiring the permission
+ # launchpad.View or launchpad.AnyAllowedPerson of milestones
+ # for the private product.
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions['launchpad.View'],
+ self.proprietary_milestone)
+ self.assertAccessAuthorzized(
+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+ self.proprietary_milestone)
+
+ # They cannot access other attributes.
+ for permission, names in self.expected_get_permissions.items():
+ if permission in (
+ CheckerPublic, 'launchpad.View',
+ 'launchpad.AnyAllowedPerson'):
+ continue
+ self.assertAccessUnauthorzized(
+ names, self.proprietary_milestone)
+
+ # They cannot change attributes.
+ for names in self.expected_set_permissions.values():
+ self.assertChangeUnauthorzized(
+ names, self.proprietary_milestone)
+
+ def test_access_for_product_owner(self):
+ # The owner of a private product can access all attributes.
+ with person_logged_in(self.proprietary_product_owner):
+ for names in self.expected_get_permissions.values():
+ self.assertAccessAuthorzized(names, self.proprietary_milestone)
+
+ # They can change attributes.
+ for permission, names in self.expected_set_permissions.items():
+ self.assertChangeAuthorzized(names, self.proprietary_milestone)
+
+
class HasMilestonesSnapshotTestCase(TestCaseWithFactory):
"""A TestCase for snapshots of pillars with milestones."""
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-10-15 09:15:29 +0000
+++ lib/lp/registry/tests/test_product.py 2012-10-16 14:36:28 +0000
@@ -523,6 +523,7 @@
CannotChangeInformationType, 'Answers is enabled.'):
product.information_type = InformationType.PROPRIETARY
+<<<<<<< TREE
def test_no_proprietary_if_packaging(self):
# information_type cannot be set to proprietary while any
# productseries are packaged.
@@ -878,6 +879,316 @@
product.userCanView(user)
product.userCanView(IPersonRoles(user))
+=======
+ expected_get_permissions = {
+ # bug=1065162 Abel Deuring 2012-10-10:
+ # name, displayname
+ # should be defined in IProductLimitedView
+ CheckerPublic: set((
+ 'id', 'information_type', 'userCanView', 'private',
+ 'name', 'displayname', 'bug_subscriptions', 'getSubscription',
+ 'getSubscriptions', 'parent_subscription_target',
+ 'target_type_display', 'userCanAlterBugSubscription',
+ 'userCanAlterSubscription', 'userHasBugSubscriptions'
+ )),
+ 'launchpad.View': set((
+ '_getOfficialTagClause', '_all_specifications',
+ '_valid_specifications', 'active', 'active_or_packaged_series',
+ 'aliases', 'all_milestones',
+ 'allowsTranslationEdits', 'allowsTranslationSuggestions',
+ 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
+ 'blueprints_usage', 'branch_sharing_policy',
+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+ 'bug_sharing_policy', 'bug_supervisor',
+ 'bug_tracking_usage', 'bugtargetdisplayname', 'bugtargetname',
+ 'bugtracker', 'canUserAlterAnswerContact',
+ 'checkPrivateBugsTransitionAllowed', 'codehosting_usage',
+ 'coming_sprints', 'commercial_subscription',
+ 'commercial_subscription_is_due', 'createBug',
+ 'createCustomLanguageCode', 'custom_language_codes',
+ 'date_next_suggest_packaging', 'datecreated', 'description',
+ 'development_focus', 'development_focusID',
+ 'direct_answer_contacts', 'distrosourcepackages',
+ 'downloadurl', 'driver', 'drivers', 'enable_bug_expiration',
+ 'enable_bugfiling_duplicate_search', 'findReferencedOOPS',
+ 'findSimilarFAQs', 'findSimilarQuestions', 'freshmeatproject',
+ 'getAllowedBugInformationTypes',
+ 'getAllowedSpecificationInformationTypes', 'getAnnouncement',
+ 'getAnnouncements', 'getAnswerContactsForLanguage',
+ 'getAnswerContactRecipients', 'getBaseBranchVisibilityRule',
+ 'getBranchVisibilityRuleForBranch',
+ 'getBranchVisibilityRuleForTeam',
+ 'getBranchVisibilityTeamPolicies', 'getBranches',
+ 'getBugSummaryContextWhereClause', 'getBugTaskWeightFunction',
+ 'getCustomLanguageCode', 'getDefaultBugInformationType',
+ 'getDefaultSpecificationInformationType',
+ 'getEffectiveTranslationPermission', 'getExternalBugTracker',
+ 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
+ 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
+ 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
+ 'getSeries', 'getSpecification',
+ 'getSupportedLanguages', 'getTimeline',
+ 'getTopContributors', 'getTopContributorsGroupedByCategory',
+ 'getTranslationGroups', 'getTranslationImportQueueEntries',
+ 'getTranslators', 'getUsedBugTagsWithOpenCounts',
+ 'getVersionSortedSeries',
+ 'has_current_commercial_subscription',
+ 'has_custom_language_codes', 'has_milestones', 'homepage_content',
+ 'homepageurl', 'icon', 'invitesTranslationEdits',
+ 'invitesTranslationSuggestions',
+ 'isUsingInheritedBranchVisibilityPolicy',
+ 'license_info', 'license_status', 'licenses', 'logo', 'milestones',
+ 'mugshot', 'name_with_project', 'newCodeImport',
+ 'obsolete_translatable_series', 'official_answers',
+ 'official_anything', 'official_blueprints', 'official_bug_tags',
+ 'official_codehosting', 'official_malone', 'owner',
+ 'packagedInDistros', 'packagings',
+ 'past_sprints', 'personHasDriverRights', 'pillar',
+ 'pillar_category', 'primary_translatable', 'private_bugs',
+ 'programminglang', 'project', 'qualifies_for_free_hosting',
+ 'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
+ 'remote_product', 'removeCustomLanguageCode',
+ 'removeTeamFromBranchVisibilityPolicy', 'screenshotsurl',
+ 'searchFAQs', 'searchQuestions', 'searchTasks', 'security_contact',
+ 'series', 'setBranchVisibilityTeamPolicy', 'setPrivateBugs',
+ 'sharesTranslationsWithOtherSide', 'sourceforgeproject',
+ 'sourcepackages', 'specification_sharing_policy', 'specifications',
+ 'sprints', 'summary', 'title',
+ 'translatable_packages', 'translatable_series',
+ 'translation_focus', 'translationgroup', 'translationgroups',
+ 'translationpermission', 'translations_usage', 'ubuntu_packages',
+ 'userCanEdit', 'uses_launchpad',
+ 'wikiurl')),
+ 'launchpad.AnyAllowedPerson': set((
+ 'addAnswerContact', 'addBugSubscription',
+ 'addBugSubscriptionFilter', 'addSubscription',
+ 'createQuestionFromBug', 'newQuestion', 'removeAnswerContact',
+ 'removeBugSubscription')),
+ 'launchpad.Append': set(('newFAQ', )),
+ 'launchpad.Driver': set(('newSeries', )),
+ 'launchpad.Edit': set((
+ 'addOfficialBugTag', 'removeOfficialBugTag',
+ 'setBranchSharingPolicy', 'setBugSharingPolicy',
+ 'setSpecificationSharingPolicy')),
+ 'launchpad.Moderate': set((
+ 'is_permitted', 'license_approved', 'project_reviewed',
+ 'reviewer_whiteboard', 'setAliases')),
+ }
+
+ def test_get_permissions(self):
+ product = self.factory.makeProduct()
+ checker = getChecker(product)
+ self.checkPermissions(
+ self.expected_get_permissions, checker.get_permissions, 'get')
+
+ def test_set_permissions(self):
+ expected_set_permissions = {
+ 'launchpad.BugSupervisor': set((
+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+ 'bugtracker', 'enable_bug_expiration',
+ 'enable_bugfiling_duplicate_search', 'official_bug_tags',
+ 'official_malone', 'remote_product')),
+ 'launchpad.Edit': set((
+ 'answers_usage', 'blueprints_usage', 'bug_supervisor',
+ 'bug_tracking_usage', 'codehosting_usage',
+ 'commercial_subscription', 'description', 'development_focus',
+ 'displayname', 'downloadurl', 'driver', 'freshmeatproject',
+ 'homepage_content', 'homepageurl', 'icon', 'information_type',
+ 'license_info', 'licenses', 'logo', 'mugshot',
+ 'official_answers', 'official_blueprints',
+ 'official_codehosting', 'owner', 'private',
+ 'programminglang', 'project', 'redeemSubscriptionVoucher',
+ 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
+ 'summary', 'title', 'uses_launchpad', 'wikiurl')),
+ 'launchpad.Moderate': set((
+ 'active', 'autoupdate', 'license_approved', 'name',
+ 'project_reviewed', 'registrant', 'reviewer_whiteboard')),
+ 'launchpad.TranslationsAdmin': set((
+ 'translation_focus', 'translationgroup',
+ 'translationpermission', 'translations_usage')),
+ 'launchpad.AnyAllowedPerson': set((
+ 'date_next_suggest_packaging', )),
+ }
+ product = self.factory.makeProduct()
+ checker = getChecker(product)
+ self.checkPermissions(
+ expected_set_permissions, checker.set_permissions, 'set')
+
+ def test_access_launchpad_View_public_product(self):
+ # Everybody, including anonymous users, has access to
+ # properties of public products that require the permission
+ # launchpad.View
+ product = self.factory.makeProduct()
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(None):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(product.owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_View_proprietary_product(self):
+ # Only people with grants for a private product can access
+ # attributes protected by the permission launchpad.View.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ with person_logged_in(owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # A user with a policy grant for the product can access attributes
+ # of a private product.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, ordinary_user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_AnyAllowedPerson_public_product(self):
+ # Only logged in persons have access to properties of public products
+ # that require the permission launchpad.AnyAllowedPerson.
+ product = self.factory.makeProduct()
+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(product.owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
+ # Only people with grants for a private product can access
+ # attributes protected by the permission launchpad.AnyAllowedPerson.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
+ with person_logged_in(None):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ self.assertRaises(
+ Unauthorized, getattr, product, attribute_name)
+ with person_logged_in(owner):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # A user with a policy grant for the product can access attributes
+ # of a private product.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, ordinary_user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(ordinary_user):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+
+ def test_set_launchpad_AnyAllowedPerson_public_product(self):
+ # Only logged in users can set attributes protected by the
+ # permission launchpad.AnyAllowedPerson.
+ product = self.factory.makeProduct()
+ with person_logged_in(None):
+ self.assertRaises(
+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
+ 'foo')
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+ with person_logged_in(product.owner):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+
+ def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
+ # Only people with grants for a private product can set
+ # attributes protected by the permission launchpad.AnyAllowedPerson.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ with person_logged_in(None):
+ self.assertRaises(
+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
+ 'foo')
+ ordinary_user = self.factory.makePerson()
+ with person_logged_in(ordinary_user):
+ self.assertRaises(
+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
+ 'foo')
+ with person_logged_in(owner):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+ # A user with a policy grant for the product can access attributes
+ # of a private product.
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, ordinary_user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(ordinary_user):
+ setattr(product, 'date_next_suggest_packaging', 'foo')
+
+ def test_userCanView_caches_known_users(self):
+ # userCanView() maintains a cache of users known to have the
+ # permission to access a product.
+ product = self.createProduct(
+ information_type=InformationType.PROPRIETARY,
+ license=License.OTHER_PROPRIETARY)
+ owner = removeSecurityProxy(product).owner
+ user = self.factory.makePerson()
+ with person_logged_in(owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ product, user, owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user):
+ with StormStatementRecorder() as recorder:
+ # The first access to a property of the product from
+ # a user requires a DB query.
+ product.homepageurl
+ queries_for_first_user_access = len(recorder.queries)
+ # The second access does not require another query.
+ product.description
+ self.assertEqual(
+ queries_for_first_user_access, len(recorder.queries))
+
+ def test_access_to_deactivated_product(self):
+ # XXX bug=1065162 Abel Deuring 2012-10-10:
+ # The properties name, displayname, parent_subscription_target must
+ # be visible for inactive products.
+ product = self.factory.makeProduct()
+ removeSecurityProxy(product).active = False
+ user = self.factory.makePerson()
+ with person_logged_in(user):
+ product.name
+ product.displayname
+ product.parent_subscription_target
+ # Access to other attributes, including those defined in
+ # IPillar, is not possible.
+ self.assertRaises(Unauthorized, getattr, product, 'active')
+ self.assertRaises(Unauthorized, getattr, product, 'title')
+
+>>>>>>> MERGE-SOURCE
class TestProductBugInformationTypes(TestCaseWithFactory):
@@ -1492,6 +1803,7 @@
self.product.owner))
self.assertTrue(
getUtility(IService, 'sharing').checkPillarAccess(
+<<<<<<< TREE
[self.product], InformationType.EMBARGOED,
self.product.owner))
@@ -1518,6 +1830,10 @@
def getSharingPolicy(self):
return self.product.specification_sharing_policy
+=======
+ [self.product], InformationType.EMBARGOED,
+ self.product.owner))
+>>>>>>> MERGE-SOURCE
class ProductSnapshotTestCase(TestCaseWithFactory):
=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
--- lib/lp/registry/tests/test_product_webservice.py 2012-10-12 14:53:10 +0000
+++ lib/lp/registry/tests/test_product_webservice.py 2012-10-16 14:36:28 +0000
@@ -70,8 +70,13 @@
def test_branch_sharing_policy_non_commercial(self):
# An API attempt to set a commercial-only branch_sharing_policy
# on a non-commercial project returns Forbidden.
+<<<<<<< TREE
product = self.factory.makeProduct()
owner = product.owner
+=======
+ product = self.factory.makeLegacyProduct()
+ owner = product.owner
+>>>>>>> MERGE-SOURCE
webservice = webservice_for_person(
product.owner, permission=OAuthPermission.WRITE_PRIVATE)
response = self.patch(
@@ -80,9 +85,14 @@
status=403,
body=('A current commercial subscription is required to use '
'proprietary branches.')))
+<<<<<<< TREE
with person_logged_in(owner):
self.assertEqual(
BranchSharingPolicy.PUBLIC, product.branch_sharing_policy)
+=======
+ with person_logged_in(owner):
+ self.assertIs(None, product.branch_sharing_policy)
+>>>>>>> MERGE-SOURCE
def test_bug_sharing_policy_can_be_set(self):
# bug_sharing_policy can be set via the API.
@@ -101,8 +111,13 @@
def test_bug_sharing_policy_non_commercial(self):
# An API attempt to set a commercial-only bug_sharing_policy
# on a non-commercial project returns Forbidden.
+<<<<<<< TREE
product = self.factory.makeProduct()
owner = product.owner
+=======
+ product = self.factory.makeLegacyProduct()
+ owner = product.owner
+>>>>>>> MERGE-SOURCE
webservice = webservice_for_person(
product.owner, permission=OAuthPermission.WRITE_PRIVATE)
response = self.patch(
@@ -111,9 +126,14 @@
status=403,
body=('A current commercial subscription is required to use '
'proprietary bugs.')))
+<<<<<<< TREE
with person_logged_in(owner):
self.assertEqual(
BugSharingPolicy.PUBLIC, product.bug_sharing_policy)
+=======
+ with person_logged_in(owner):
+ self.assertIs(None, product.bug_sharing_policy)
+>>>>>>> MERGE-SOURCE
def fetch_product(self, webservice, product, api_version):
with person_logged_in(webservice.user):
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2012-10-12 14:53:10 +0000
+++ lib/lp/security.py 2012-10-16 14:36:28 +0000
@@ -425,6 +425,7 @@
return user.isOwner(self.obj) or user.in_admin
+<<<<<<< TREE
class ViewProduct(AuthorizationBase):
permission = 'launchpad.View'
usedfor = IProduct
@@ -449,6 +450,36 @@
return False
+=======
+class ViewProduct(ViewPillar):
+ permission = 'launchpad.View'
+ usedfor = IProduct
+
+ def checkAuthenticated(self, user):
+ return (
+ super(ViewProduct, self).checkAuthenticated(user) and
+ self.obj.userCanView(user))
+
+ def checkUnauthenticated(self):
+ return (
+ self.obj.information_type in PUBLIC_INFORMATION_TYPES and
+ super(ViewProduct, self).checkUnauthenticated())
+
+
+class ChangeProduct(ViewProduct):
+ """Used for attributes of IProduct that are accessible to any logged
+ in user for public product but only to persons with access grants
+ for private products.
+ """
+
+ permission = 'launchpad.AnyAllowedPerson'
+ usedfor = IProduct
+
+ def checkUnauthenticated(self):
+ return False
+
+
+>>>>>>> MERGE-SOURCE
class EditProduct(EditByOwnersOrAdmins):
usedfor = IProduct
@@ -727,6 +758,25 @@
return False
+class ViewMilestone(AuthorizationBase):
+ permission = 'launchpad.View'
+ usedfor = IMilestone
+
+ def checkAuthenticated(self, user):
+ return self.obj.userCanView(user)
+
+ def checkUnauthenticated(self):
+ return self.obj.userCanView(user=None)
+
+
+class EditMilestone(ViewMilestone):
+ permission = 'launchpad.AnyAllowedPerson'
+ usedfor = IMilestone
+
+ def checkUnauthenticated(self):
+ return False
+
+
class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
permission = 'launchpad.Edit'
usedfor = IMilestone
=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2012-09-05 14:44:17 +0000
+++ lib/lp/testing/__init__.py 2012-10-16 14:36:28 +0000
@@ -704,6 +704,36 @@
raise AssertionError(
'string %r does not end with %r' % (s, suffix))
+ def checkPermissions(self, expected_permissions, used_permissions,
+ type_):
+ """Check if the used_permissions match expected_permissions.
+
+ :param expected_permissions: A dictionary mapping a permission
+ to a set of attribute names.
+ :param used_permissions: The property get_permissions or
+ set_permissions of getChecker(security_proxied_object).
+ :param type_: The string "set" or "get".
+ """
+ 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,
+ sorted(
+ attribute_names - expected_permissions[permission]),
+ sorted(
+ expected_permissions[permission] - attribute_names)))
+
class TestCaseWithFactory(TestCase):
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-10-15 23:20:25 +0000
+++ lib/lp/testing/factory.py 2012-10-16 14:36:28 +0000
@@ -1025,6 +1025,23 @@
specification_sharing_policy)
if information_type is not None:
naked_product.information_type = information_type
+<<<<<<< TREE
+=======
+ return product
+
+ def makeLegacyProduct(self, **kwargs):
+ # Create a product which does not have any of the new bug and branch
+ # sharing policies set. New products have these set to default values
+ # but we need to test for existing products which have not yet been
+ # migrated.
+ # XXX This method can be removed when branch visibility policy dies.
+ product = self.makeProduct(**kwargs)
+ # Since createProduct() doesn't create PRIVATESECURITY/USERDATA.
+ removeSecurityProxy(product)._ensurePolicies([
+ InformationType.PRIVATESECURITY, InformationType.USERDATA])
+ removeSecurityProxy(product).bug_sharing_policy = None
+ removeSecurityProxy(product).branch_sharing_policy = None
+>>>>>>> MERGE-SOURCE
return product
def makeProductSeries(self, product=None, name=None, owner=None,