launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13199
[Merge] lp:~abentley/launchpad/private-product-listings into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/private-product-listings into lp:launchpad.
Commit message:
Do not attempt to list Products for users who cannot view them.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~abentley/launchpad/private-product-listings/+merge/128800
= Summary =
Fix bug #1063264: Cannot view /projects when a private project is in the list
== Proposed fix ==
List only those products that the user can view.
== Pre-implementation notes ==
None
== LOC Rationale ==
Part of Private Projects
== Implementation details ==
Launchpad admins and commercial admins should be able to review all products. This is handled in getProductPrivacyFilter, but Product.userCanView did not implement this, so it was added.
ProductSetView.latest was implemented so that it could supply self.user to get_all_active.
== Tests ==
bin/test -t getProductPrivacyFilter -t test_get_all_active_omits_proprietary -t
test_admin_launchpad_View_proprietary_product -t test_review_include_proprietary_for_admin -t test_review_exclude_proprietary_for_expert -t test_proprietary_products_shown_to_owners_all -t test_proprietary_products_skipped_all -t test_proprietary_products_shown_to_owners -t test_proprietary_products_skipped
== Demo and Q/A ==
Create a proprietary product. It should be visible in /projects and projects/+all. Log in as a different user. It should not be visible. Share the product with another user. It should be visible to that user.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/registry/doc/commercialsubscription.txt
lib/lp/registry/browser/product.py
lib/lp/registry/browser/tests/test_product.py
lib/lp/registry/model/product.py
lib/lp/registry/templates/products-index.pt
lib/lp/registry/tests/test_product.py
--
https://code.launchpad.net/~abentley/launchpad/private-product-listings/+merge/128800
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/private-product-listings into lp:launchpad.
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-10-06 23:40:20 +0000
+++ lib/lp/registry/browser/product.py 2012-10-09 19:39:21 +0000
@@ -1721,7 +1721,8 @@
@cachedproperty
def all_batched(self):
- return BatchNavigator(self.context.all_active, self.request)
+ return BatchNavigator(self.context.get_all_active(self.user),
+ self.request)
@cachedproperty
def matches(self):
@@ -1739,6 +1740,9 @@
def tooManyResultsFound(self):
return self.matches > self.max_results_to_display
+ def latest(self):
+ return self.context.get_all_active(self.user)[:5]
+
class ProductSetReviewLicensesView(LaunchpadFormView):
"""View for searching products to be reviewed."""
@@ -1814,8 +1818,8 @@
search_params = self.initial_values
# Override the defaults with the form values if available.
search_params.update(data)
- return BatchNavigator(self.context.forReview(**search_params),
- self.request, size=50)
+ result = self.context.forReview(self.user, **search_params)
+ return BatchNavigator(result, self.request, size=50)
class ProductAddViewBase(ProductLicenseMixin, LaunchpadFormView):
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2012-10-09 19:39:21 +0000
@@ -552,3 +552,82 @@
content_disposition, browser.headers['Content-disposition'])
self.assertEqual(
'application/rdf+xml', browser.headers['Content-type'])
+
+
+class TestProductSet(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def makeAllInformationTypes(self):
+ owner = self.factory.makePerson()
+ public = self.factory.makeProduct(
+ information_type=InformationType.PUBLIC, owner=owner)
+ proprietary = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY, owner=owner)
+ embargoed = self.factory.makeProduct(
+ information_type=InformationType.EMBARGOED, owner=owner)
+ return owner, public, proprietary, embargoed
+
+ def test_proprietary_products_skipped(self):
+ # Ignore proprietary products for anonymous users
+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
+ browser = self.getViewBrowser(getUtility(IProductSet))
+ with person_logged_in(owner):
+ self.assertIn(public.name, browser.contents)
+ self.assertNotIn(proprietary.name, browser.contents)
+ self.assertNotIn(embargoed.name, browser.contents)
+
+ def test_proprietary_products_shown_to_owners(self):
+ # Owners will see their proprietary products listed
+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
+ transaction.commit()
+ browser = self.getViewBrowser(getUtility(IProductSet), user=owner)
+ with person_logged_in(owner):
+ self.assertIn(public.name, browser.contents)
+ self.assertIn(proprietary.name, browser.contents)
+ self.assertIn(embargoed.name, browser.contents)
+
+ def test_proprietary_products_skipped_all(self):
+ # Ignore proprietary products for anonymous users
+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
+ product_set = getUtility(IProductSet)
+ browser = self.getViewBrowser(product_set, view_name='+all')
+ with person_logged_in(owner):
+ self.assertIn(public.name, browser.contents)
+ self.assertNotIn(proprietary.name, browser.contents)
+ self.assertNotIn(embargoed.name, browser.contents)
+
+ def test_proprietary_products_shown_to_owners_all(self):
+ # Owners will see their proprietary products listed
+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
+ transaction.commit()
+ browser = self.getViewBrowser(getUtility(IProductSet), user=owner,
+ view_name='+all')
+ with person_logged_in(owner):
+ self.assertIn(public.name, browser.contents)
+ self.assertIn(proprietary.name, browser.contents)
+ self.assertIn(embargoed.name, browser.contents)
+
+ def test_review_exclude_proprietary_for_expert(self):
+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
+ transaction.commit()
+ expert = self.factory.makeRegistryExpert()
+ browser = self.getViewBrowser(getUtility(IProductSet),
+ view_name='+review-licenses',
+ user=expert)
+ with person_logged_in(owner):
+ self.assertIn(public.name, browser.contents)
+ self.assertNotIn(proprietary.name, browser.contents)
+ self.assertNotIn(embargoed.name, browser.contents)
+
+ def test_review_include_proprietary_for_admin(self):
+ owner, public, proprietary, embargoed = self.makeAllInformationTypes()
+ transaction.commit()
+ admin = self.factory.makeAdministrator()
+ browser = self.getViewBrowser(getUtility(IProductSet),
+ view_name='+review-licenses',
+ user=admin)
+ with person_logged_in(owner):
+ self.assertIn(public.name, browser.contents)
+ self.assertIn(proprietary.name, browser.contents)
+ self.assertIn(embargoed.name, browser.contents)
=== modified file 'lib/lp/registry/doc/commercialsubscription.txt'
--- lib/lp/registry/doc/commercialsubscription.txt 2012-07-07 13:01:46 +0000
+++ lib/lp/registry/doc/commercialsubscription.txt 2012-10-09 19:39:21 +0000
@@ -351,7 +351,8 @@
>>> from datetime import timedelta
>>> bzr.licenses = [License.GNU_GPL_V2, License.ECLIPSE]
>>> flush_database_updates()
- >>> for product in product_set.forReview(search_text='gnome'):
+ >>> for product in product_set.forReview(commercial_member,
+ ... search_text='gnome'):
... print product.displayname
python gnome2 dev
Evolution
@@ -362,7 +363,8 @@
The license_info field is also searched for matching search_text:
>>> bzr.license_info = 'Code in /contrib is under a mit-like licence.'
- >>> for product in product_set.forReview(search_text='mit'):
+ >>> for product in product_set.forReview(commercial_member,
+ ... search_text='mit'):
... print product.name
bzr
@@ -372,20 +374,22 @@
>>> with celebrity_logged_in('registry_experts'):
... bzr.reviewer_whiteboard = (
... 'cc-nc discriminates against commercial uses.')
- >>> for product in product_set.forReview(search_text='cc-nc'):
+ >>> for product in product_set.forReview(commercial_member,
+ ... search_text='cc-nc'):
... print product.name
bzr
You can search for whether the product is active or not.
- >>> for product in product_set.forReview(active=False):
+ >>> for product in product_set.forReview(commercial_member, active=False):
... print product.name
python-gnome2-dev
unassigned
You can search for whether the product is marked reviewed or not.
- >>> for product in product_set.forReview(project_reviewed=True):
+ >>> for product in product_set.forReview(commercial_member,
+ ... project_reviewed=True):
... print product.name
python-gnome2-dev
unassigned
@@ -396,6 +400,7 @@
any one of the licences listed.
>>> for product in product_set.forReview(
+ ... commercial_member,
... licenses=[License.GNU_GPL_V2, License.BSD]):
... print product.name
bzr
@@ -404,6 +409,7 @@
not approved
>>> for product in product_set.forReview(
+ ... commercial_member,
... project_reviewed=True, license_approved=False):
... print product.name
python-gnome2-dev
@@ -414,6 +420,7 @@
was created.
>>> for product in product_set.forReview(
+ ... commercial_member,
... search_text='bzr',
... created_after=bzr.datecreated,
... created_before=bzr.datecreated):
@@ -425,6 +432,7 @@
>>> date_expires = bzr.commercial_subscription.date_expires
>>> for product in product_set.forReview(
+ ... commercial_member,
... search_text='bzr',
... subscription_expires_after=date_expires,
... subscription_expires_before=date_expires):
@@ -439,6 +447,7 @@
>>> early_date = date(1980, 1, 1)
>>> late_date = date_expires + timedelta(days=365 * 100)
>>> for product in product_set.forReview(
+ ... commercial_member,
... search_text='bzr',
... subscription_expires_after=date_expires,
... subscription_expires_before=date_expires + one_day,
@@ -452,6 +461,7 @@
A reviewer can search for projects without a commercial subscription.
>>> for product in product_set.forReview(
+ ... commercial_member,
... has_subscription=False, licenses=[License.OTHER_PROPRIETARY]):
... print product.name
mega-money-maker
@@ -462,6 +472,7 @@
>>> date_last_modified = bzr.commercial_subscription.date_last_modified
>>> for product in product_set.forReview(
+ ... commercial_member,
... search_text='bzr',
... subscription_modified_after=date_last_modified,
... subscription_modified_before=date_last_modified):
@@ -471,14 +482,16 @@
All the products are returned when no parameters are passed in.
>>> from lp.registry.model.product import Product
- >>> product_set.forReview().count() == Product.select().count()
+ >>> review_listing = product_set.forReview(commercial_member)
+ >>> review_listing.count() == Product.select().count()
True
The full text search will not match strings with dots in their name
but a clause is included to search specifically for the name.
>>> new_product = factory.makeProduct(name="abc.com")
- >>> for product in product_set.forReview(search_text="abc.com"):
+ >>> for product in product_set.forReview(commercial_member,
+ ... search_text="abc.com"):
... print product.name
abc.com
@@ -488,7 +501,7 @@
>>> login('no-priv@xxxxxxxxxxxxx')
>>> check_permission('launchpad.Moderate', product_set)
False
- >>> gnome = product_set.forReview(search_text='gnome')
+ >>> gnome = product_set.forReview(commercial_member, search_text='gnome')
Traceback (most recent call last):
...
Unauthorized:... 'forReview', 'launchpad.Moderate'...
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/model/product.py 2012-10-09 19:39:21 +0000
@@ -55,6 +55,10 @@
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
+from lp.registry.model.accesspolicy import (
+ AccessPolicy,
+ AccessPolicyGrantFlat,
+ )
from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
from lp.answers.interfaces.faqtarget import IFAQTarget
from lp.answers.model.faq import (
@@ -175,6 +179,7 @@
from lp.registry.model.productlicense import ProductLicense
from lp.registry.model.productrelease import ProductRelease
from lp.registry.model.productseries import ProductSeries
+from lp.registry.model.teammembership import TeamParticipation
from lp.registry.model.series import ACTIVE_STATUSES
from lp.registry.model.sourcepackagename import SourcePackageName
from lp.services.database import bulk
@@ -1525,6 +1530,7 @@
return weight_function
+<<<<<<< TREE
def get_precached_products(products, need_licences=False, need_projects=False,
need_series=False, need_releases=False,
@@ -1669,6 +1675,35 @@
result = result.order_by(SourcePackageName.name, Distribution.name)
result.config(distinct=True)
return result
+=======
+ @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
+ roles = IPersonRoles(user)
+ if roles.in_admin or roles.in_commercial_admin:
+ 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
class ProductSet:
@@ -1700,11 +1735,28 @@
@property
def all_active(self):
- return self.get_all_active()
-
- def get_all_active(self, eager_load=True):
- result = IStore(Product).find(Product, Product.active
- ).order_by(Desc(Product.datecreated))
+ return self.get_all_active(None)
+
+ @staticmethod
+ def getProductPrivacyFilter(user):
+ if user is not None:
+ roles = IPersonRoles(user)
+ if roles.in_admin or roles.in_commercial_admin:
+ return True
+ granted_products = And(
+ AccessPolicyGrantFlat.grantee_id == TeamParticipation.teamID,
+ TeamParticipation.person == user,
+ AccessPolicyGrantFlat.policy == AccessPolicy.id,
+ AccessPolicy.product == Product.id,
+ AccessPolicy.type == Product._information_type)
+ return Or(Product._information_type == InformationType.PUBLIC,
+ Product.id.is_in(Select(Product.id, granted_products)))
+
+ @classmethod
+ def get_all_active(cls, user, eager_load=True):
+ clause = cls.getProductPrivacyFilter(user)
+ result = IStore(Product).find(Product, Product.active,
+ clause).order_by(Desc(Product.datecreated))
if not eager_load:
return result
@@ -1814,7 +1866,7 @@
product.development_focus = trunk
return product
- def forReview(self, search_text=None, active=None,
+ def forReview(self, user, search_text=None, active=None,
project_reviewed=None, license_approved=None, licenses=None,
created_after=None, created_before=None,
has_subscription=None,
@@ -1824,7 +1876,7 @@
subscription_modified_before=None):
"""See lp.registry.interfaces.product.IProductSet."""
- conditions = []
+ conditions = [self.getProductPrivacyFilter(user)]
if project_reviewed is not None:
conditions.append(Product.project_reviewed == project_reviewed)
=== modified file 'lib/lp/registry/templates/products-index.pt'
--- lib/lp/registry/templates/products-index.pt 2012-06-02 12:25:39 +0000
+++ lib/lp/registry/templates/products-index.pt 2012-10-09 19:39:21 +0000
@@ -132,7 +132,7 @@
<div class="portlet">
<h2>Latest projects registered</h2>
<table>
- <tr tal:repeat="product context/latest">
+ <tr tal:repeat="product view/latest">
<td>
<tal:link replace="structure product/fmt:link" />
registered
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-10-08 10:07:11 +0000
+++ lib/lp/registry/tests/test_product.py 2012-10-09 19:39:21 +0000
@@ -62,9 +62,11 @@
from lp.registry.interfaces.series import SeriesStatus
from lp.registry.model.product import (
Product,
+ ProductSet,
UnDeactivateable,
)
from lp.registry.model.productlicense import ProductLicense
+from lp.services.database.lpstorm import IStore
from lp.services.webapp.authorization import check_permission
from lp.testing import (
celebrity_logged_in,
@@ -566,6 +568,329 @@
CannotChangeInformationType, 'Answers is enabled.'):
product.information_type = InformationType.PROPRIETARY
+<<<<<<< TREE
+=======
+ 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,
+ sorted(
+ attribute_names - expected_permissions[permission]),
+ sorted(
+ expected_permissions[permission] - attribute_names)))
+
+ expected_get_permissions = {
+ CheckerPublic: set((
+ 'active', 'id', 'information_type', 'pillar_category', 'private',
+ 'userCanView',)),
+ 'launchpad.View': set((
+ '_getOfficialTagClause', '_all_specifications',
+ '_valid_specifications', '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_subscriptions', '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', 'displayname', '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', 'getSubscription',
+ 'getSubscriptions', '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', 'name_with_project', 'newCodeImport',
+ 'obsolete_translatable_series', 'official_answers',
+ 'official_anything', 'official_blueprints', 'official_bug_tags',
+ 'official_codehosting', 'official_malone', 'owner',
+ 'parent_subscription_target', 'packagedInDistros', 'packagings',
+ 'past_sprints', 'personHasDriverRights', 'pillar',
+ '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', 'target_type_display', 'title',
+ 'translatable_packages', 'translatable_series',
+ 'translation_focus', 'translationgroup', 'translationgroups',
+ 'translationpermission', 'translations_usage', 'ubuntu_packages',
+ 'userCanAlterBugSubscription', 'userCanAlterSubscription',
+ 'userCanEdit', 'userHasBugSubscriptions', '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.check_permissions(
+ 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.check_permissions(
+ 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_admin_launchpad_View_proprietary_product(self):
+ # Admins and commercial admins can view proprietary products.
+ product = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ names = self.expected_get_permissions['launchpad.View']
+ with person_logged_in(self.factory.makeAdministrator()):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ with person_logged_in(self.factory.makeCommercialAdmin()):
+ 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))
+
+>>>>>>> MERGE-SOURCE
class TestProductBugInformationTypes(TestCaseWithFactory):
@@ -1303,3 +1628,115 @@
self.failUnlessEqual(
[],
ws_product.findReferencedOOPS(start_date=now - day, end_date=now))
+
+
+class TestProductSet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def makeAllInformationTypes(self):
+ proprietary = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ embargoed = self.factory.makeProduct(
+ information_type=InformationType.EMBARGOED)
+ public = self.factory.makeProduct(
+ information_type=InformationType.PUBLIC)
+ return proprietary, embargoed, public
+
+ @staticmethod
+ def filterFind(user):
+ clause = ProductSet.getProductPrivacyFilter(user)
+ return IStore(Product).find(Product, clause)
+
+ def test_get_all_active_omits_proprietary(self):
+ # Ignore proprietary products for anonymous users
+ proprietary = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY)
+ embargoed = self.factory.makeProduct(
+ information_type=InformationType.EMBARGOED)
+ result = ProductSet.get_all_active(None)
+ self.assertNotIn(proprietary, result)
+ self.assertNotIn(embargoed, result)
+
+ def test_getProductPrivacyFilterAnonymous(self):
+ # Ignore proprietary products for anonymous users
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(None)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_excludes_random_users(self):
+ # Exclude proprietary products for anonymous users
+ random = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(random)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def grant(self, pillar, information_type, grantee):
+ policy_source = getUtility(IAccessPolicySource)
+ (policy,) = policy_source.find(
+ [(pillar, information_type)])
+ self.factory.makeAccessPolicyGrant(policy, grantee)
+
+ def test_getProductPrivacyFilter_respects_grants(self):
+ # Include proprietary products for users with right grants.
+ grantee = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.grant(embargoed, InformationType.EMBARGOED, grantee)
+ self.grant(proprietary, InformationType.PROPRIETARY, grantee)
+ result = self.filterFind(grantee)
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_ignores_wrong_product(self):
+ # Exclude proprietary products if grant is on wrong product.
+ grantee = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.factory.makeAccessPolicyGrant(grantee=grantee)
+ result = self.filterFind(grantee)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_ignores_wrong_info_type(self):
+ # Exclude proprietary products if grant is on wrong information type.
+ grantee = self.factory.makePerson()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.grant(embargoed, InformationType.PROPRIETARY, grantee)
+ self.factory.makeAccessPolicy(proprietary, InformationType.EMBARGOED)
+ self.grant(proprietary, InformationType.EMBARGOED, grantee)
+ result = self.filterFind(grantee)
+ self.assertIn(public, result)
+ self.assertNotIn(embargoed, result)
+ self.assertNotIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_respects_team_grants(self):
+ # Include proprietary products for users in teams with right grants.
+ grantee = self.factory.makeTeam()
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ self.grant(embargoed, InformationType.EMBARGOED, grantee)
+ self.grant(proprietary, InformationType.PROPRIETARY, grantee)
+ result = self.filterFind(grantee.teamowner)
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_includes_admins(self):
+ # Launchpad admins can see everything.
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(self.factory.makeAdministrator())
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
+
+ def test_getProductPrivacyFilter_includes_commercial_admins(self):
+ # Commercial admins can see everything.
+ proprietary, embargoed, public = self.makeAllInformationTypes()
+ result = self.filterFind(self.factory.makeCommercialAdmin())
+ self.assertIn(public, result)
+ self.assertIn(embargoed, result)
+ self.assertIn(proprietary, result)
Follow ups