Merge lp:~rharding/launchpad/related_projects_1063272 into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merged at revision: 16185
Proposed branch: lp:~rharding/launchpad/related_projects_1063272
Merge into: lp:launchpad
Diff against target: 520 lines (+269/-33)
8 files modified
lib/lp/registry/browser/person.py (+10/-5)
lib/lp/registry/doc/person-account.txt (+3/-3)
lib/lp/registry/interfaces/person.py (+18/-2)
lib/lp/registry/interfaces/product.py (+7/-0)
lib/lp/registry/model/person.py (+109/-18)
lib/lp/registry/model/product.py (+9/-0)
lib/lp/registry/tests/test_person.py (+97/-5)
lib/lp/registry/tests/test_product.py (+16/-0)
To merge this branch: bzr merge lp:~rharding/launchpad/related_projects_1063272
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+130414@code.launchpad.net

Commit message

Filter affiliatedPillars based on access grants to hide non-visible products.

Description of the change

= Summary =

The person getAffiliatedPillars doesn't take into account products that are
not visible to the user. This adds the same checks as
product.getProductPrivacyFilter in order to clean that data.

== Pre Implementation ==

Talked with Orange about if this could/should be converted to storm or
performed with raw sql adjustments. The storm/union/sorting issues kept it as
raw sql at this time.

Talked with Curtis and Deryck around handling the account deactivation issue
this ran smack into.

== Implementation Notes ==

Due to the nature of the query here, with unions that must be in a specific
sorted order, the product method could not be used. It's logic was turned into
raw sql and used to filter out the product part of the union.

This makes sure that commercial admins and admins maintain access/visibility
by passing in the current viewing user to the underlying methods in order to
check for permissions.

_genAffiliatedProductSql is created to contain the logic about how the product
part of the union is to be done. It uses the old query if you're an
admin/commercial admin, however it filters based on granted products
otherwise. See product.getPrivacyFilter for the logic that was copied to
construct this query.

As a side effect, we ran into the fact that account deactivation uses
getAffiliatedPillars, which we've updated.

The decision was made that accounts that are owners of non-public products
cannot be deactivated. This needed to be added to view validation. In order to
prevent duplicate code, the validation was added to the person model. It
checks for a list of non-public products owned by the user. For validation
purposes, we needed two methods. One that's just a boolean T/F, the second
however, is needed to get readable error message back to the view to be placed
in front of the user. Because of this we've added both canDeactivateUser and
canDeactivateUserWithErrors.

The validation checking in Person required a new query on Product so that we
can easily and quickly get the list of non-public products. We needed to do a
full query to get the list so we can provide the names of those products to
the user in the validation error message.

The final trick was to add a new can_deactivate kwarg so that we can prevent
the query being hit twice, once in the view and once in the model. Since the
view did the check it can let the model know it's been sanity checked already.

There's a drive by change requested by Curtis. There's no sense setting the bug supervisor or the driver to registry experts. They are nullable values. So we set them to null and only change the owner to help with registry spam.

== QA ==

Log in as a user and create a private product. Then visit the user's
+related-projects page and it should load just fine.

Log out and as an anonymous user also load the same url without issue. No
private products should be visible to the anonymous user.

Deactivating the user with the private product should not be permitted. It
must be re-assigned to another user before deactivation is permitted.

== Tests ==

registry/tests/test_person.py
registry/tests/test_product.py

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Rick,

Some comments on your branch:

* In genAffiliatedProductSql there is a lot of repetition in the queries that could be factored out so you DRY as the first query you return is a the same as the final one minus one clause. Give a shot at refactoring if you don't mind.

* Thanks for the nice error messages when the account cannot be deactivated.

* The tests are clear and easy to follow. Thanks.

review: Approve (code)
Revision history for this message
Richard Harding (rharding) wrote :

> Hi Rick,
>
> Some comments on your branch:
>
> * In genAffiliatedProductSql there is a lot of repetition in the queries that
> could be factored out so you DRY as the first query you return is a the same
> as the final one minus one clause. Give a shot at refactoring if you don't
> mind.

Thanks for the catch. Was a series of iterations over that and didn't get back to cleaning it up. I've reused the base_query for the second build now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/person.py'
2--- lib/lp/registry/browser/person.py 2012-10-22 02:30:44 +0000
3+++ lib/lp/registry/browser/person.py 2012-10-24 09:24:22 +0000
4@@ -635,7 +635,8 @@
5 def projects(self):
6 target = '+related-projects'
7 text = 'Related projects'
8- enabled = bool(self.person.getAffiliatedPillars())
9+ user = getUtility(ILaunchBag).user
10+ enabled = bool(self.person.getAffiliatedPillars(user))
11 return Link(target, text, enabled=enabled, icon='info')
12
13 def subscriptions(self):
14@@ -936,12 +937,15 @@
15
16 def validate(self, data):
17 """See `LaunchpadFormView`."""
18- if self.context.account_status != AccountStatus.ACTIVE:
19- self.addError('This account is already deactivated.')
20+ can_deactivate, errors = self.context.canDeactivateAccountWithErrors()
21+ if not can_deactivate:
22+ [self.addError(message) for message in errors]
23
24 @action(_("Deactivate My Account"), name="deactivate")
25 def deactivate_action(self, action, data):
26- self.context.deactivateAccount(data['comment'])
27+ # We override the can_deactivate since validation already processed
28+ # this information.
29+ self.context.deactivateAccount(data['comment'], can_deactivate=True)
30 logoutPerson(self.request)
31 self.request.response.addInfoNotification(
32 _(u'Your account has been deactivated.'))
33@@ -3517,7 +3521,8 @@
34 @cachedproperty
35 def _related_projects(self):
36 """Return all projects owned or driven by this person."""
37- return self.context.getAffiliatedPillars()
38+ user = getUtility(ILaunchBag).user
39+ return self.context.getAffiliatedPillars(user)
40
41 def _tableHeaderMessage(self, count, label='package'):
42 """Format a header message for the tables on the summary page."""
43
44=== modified file 'lib/lp/registry/doc/person-account.txt'
45--- lib/lp/registry/doc/person-account.txt 2012-09-28 14:35:35 +0000
46+++ lib/lp/registry/doc/person-account.txt 2012-10-24 09:24:22 +0000
47@@ -104,7 +104,7 @@
48 True
49
50 >>> foobar_pillars = []
51- >>> for pillar_name in foobar.getAffiliatedPillars():
52+ >>> for pillar_name in foobar.getAffiliatedPillars(foobar):
53 ... pillar = pillar_name.pillar
54 ... if pillar.owner == foobar or pillar.driver == foobar:
55 ... foobar_pillars.append(pillar_name)
56@@ -183,7 +183,7 @@
57
58 ...no owned or driven pillars...
59
60- >>> foobar.getAffiliatedPillars().count()
61+ >>> foobar.getAffiliatedPillars(foobar).count()
62 0
63
64 ...and, finally, to not be considered a valid person in Launchpad.
65@@ -198,7 +198,7 @@
66 >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
67 >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
68
69- >>> registry_pillars = set(registry_experts.getAffiliatedPillars())
70+ >>> registry_pillars = set(registry_experts.getAffiliatedPillars(foobar))
71 >>> registry_pillars.issuperset(foobar_pillars)
72 True
73
74
75=== modified file 'lib/lp/registry/interfaces/person.py'
76--- lib/lp/registry/interfaces/person.py 2012-10-16 00:48:55 +0000
77+++ lib/lp/registry/interfaces/person.py 2012-10-24 09:24:22 +0000
78@@ -1132,7 +1132,7 @@
79 the icons which represent that category.
80 """
81
82- def getAffiliatedPillars():
83+ def getAffiliatedPillars(user):
84 """Return the pillars that this person directly has a role with.
85
86 Returns distributions, project groups, and projects that this person
87@@ -1729,7 +1729,20 @@
88 class IPersonSpecialRestricted(Interface):
89 """IPerson methods that require launchpad.Special permission to use."""
90
91- def deactivateAccount(comment):
92+ def canDeactivateAccount():
93+ """Verify we safely deactivate this user account.
94+
95+ :return: True if the person can be deactivated, False otherwise.
96+ """
97+
98+ def canDeactivateAccountWithErrors():
99+ """See canDeactivateAccount with the addition of error messages for
100+ why the account cannot be deactivated.
101+
102+ :return tuple: boolean, list of error messages.
103+ """
104+
105+ def deactivateAccount(comment, can_deactivate=None):
106 """Deactivate this person's Launchpad account.
107
108 Deactivating an account means:
109@@ -1740,6 +1753,9 @@
110 - Changing the ownership of products/projects/teams owned by him.
111
112 :param comment: An explanation of why the account status changed.
113+ :param can_deactivate: Override the check if we can deactivate by
114+ supplying a known value. If None, then the method will run the
115+ checks.
116 """
117
118 def reactivate(comment, preferred_email):
119
120=== modified file 'lib/lp/registry/interfaces/product.py'
121--- lib/lp/registry/interfaces/product.py 2012-10-16 13:59:06 +0000
122+++ lib/lp/registry/interfaces/product.py 2012-10-24 09:24:22 +0000
123@@ -942,6 +942,13 @@
124 all_active = Attribute(
125 "All the active products, sorted newest first.")
126
127+ def get_users_private_products(user):
128+ """Get users non-public products.
129+
130+ :param user: Which user are we searching products for.
131+ :return: An iterable of IProduct
132+ """
133+
134 def get_all_active(eager_load=True):
135 """Get all active products.
136
137
138=== modified file 'lib/lp/registry/model/person.py'
139--- lib/lp/registry/model/person.py 2012-10-22 02:30:44 +0000
140+++ lib/lp/registry/model/person.py 2012-10-24 09:24:22 +0000
141@@ -109,7 +109,10 @@
142
143 from lp import _
144 from lp.answers.model.questionsperson import QuestionsPersonMixin
145-from lp.app.enums import PRIVATE_INFORMATION_TYPES
146+from lp.app.enums import (
147+ InformationType,
148+ PRIVATE_INFORMATION_TYPES,
149+ )
150 from lp.app.interfaces.launchpad import (
151 IHasIcon,
152 IHasLogo,
153@@ -202,7 +205,10 @@
154 from lp.registry.interfaces.personnotification import IPersonNotificationSet
155 from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
156 from lp.registry.interfaces.pillar import IPillarNameSet
157-from lp.registry.interfaces.product import IProduct
158+from lp.registry.interfaces.product import (
159+ IProduct,
160+ IProductSet,
161+ )
162 from lp.registry.interfaces.projectgroup import IProjectGroup
163 from lp.registry.interfaces.role import IPersonRoles
164 from lp.registry.interfaces.ssh import (
165@@ -1030,19 +1036,64 @@
166 cur.execute(query)
167 return cur.fetchall()
168
169- def getAffiliatedPillars(self):
170+ def _genAffiliatedProductSql(self, user=None):
171+ """Helper to generate the product sql for getAffiliatePillars"""
172+ base_query = """
173+ SELECT name, 3 as kind, displayname
174+ FROM product p
175+ WHERE
176+ p.active = True
177+ AND (
178+ p.driver = %(person)s
179+ OR p.owner = %(person)s
180+ OR p.bug_supervisor = %(person)s
181+ )
182+ """ % sqlvalues(person=self)
183+
184+ if user is not None:
185+ roles = IPersonRoles(user)
186+ if roles.in_admin or roles.in_commercial_admin:
187+ return base_query
188+
189+ # This is the raw sql version of model/product getProductPrivacyFilter
190+ granted_products = """
191+ SELECT p.id
192+ FROM product p,
193+ accesspolicygrantflat apflat,
194+ teamparticipation part,
195+ accesspolicy ap
196+ WHERE
197+ apflat.grantee = part.team
198+ AND part.person = %(user)s
199+ AND apflat.policy = ap.id
200+ AND ap.product = p.id
201+ AND ap.type = p.information_type
202+ """ % sqlvalues(user=user)
203+
204+ # We have to generate the sqlvalues first so that they're properly
205+ # setup and escaped. Then we combine the above query which is already
206+ # processed.
207+ query_values = sqlvalues(information_type=InformationType.PUBLIC)
208+ query_values.update(granted_sql=granted_products)
209+
210+ query = base_query + """
211+ AND (
212+ p.information_type = %(information_type)s
213+ OR p.information_type is NULL
214+ OR p.id IN (%(granted_sql)s)
215+ )
216+ """ % query_values
217+ return query
218+
219+ def getAffiliatedPillars(self, user):
220 """See `IPerson`."""
221 find_spec = (PillarName, SQL('kind'), SQL('displayname'))
222- origin = SQL("""
223- PillarName
224- JOIN (
225- SELECT name, 3 as kind, displayname
226- FROM product
227- WHERE
228- active = True AND
229- (driver = %(person)s
230- OR owner = %(person)s
231- OR bug_supervisor = %(person)s)
232+ base = """PillarName
233+ JOIN (
234+ %s
235+ """ % self._genAffiliatedProductSql(user=user)
236+
237+ origin = base + """
238 UNION
239 SELECT name, 2 as kind, displayname
240 FROM project
241@@ -1059,8 +1110,9 @@
242 OR bug_supervisor = %(person)s
243 ) _pillar
244 ON PillarName.name = _pillar.name
245- """ % sqlvalues(person=self))
246- results = IStore(self).using(origin).find(find_spec)
247+ """ % sqlvalues(person=self)
248+
249+ results = IStore(self).using(SQL(origin)).find(find_spec)
250 results = results.order_by('kind', 'displayname')
251
252 def get_pillar_name(result):
253@@ -2109,15 +2161,47 @@
254 clauseTables=['Person'],
255 orderBy=Person.sortingColumns)
256
257+ def canDeactivateAccount(self):
258+ """See `IPerson`."""
259+ can_deactivate, errors = self.canDeactivateAccountWithErrors()
260+ return can_deactivate
261+
262+ def canDeactivateAccountWithErrors(self):
263+ """See `IPerson`."""
264+ # Users that own non-public products cannot be deactivated until the
265+ # products are reassigned.
266+ errors = []
267+ product_set = getUtility(IProductSet)
268+ non_public_products = product_set.get_users_private_products(self)
269+ if non_public_products.count() != 0:
270+ errors.append(('This account cannot be deactivated because it owns '
271+ 'the following non-public products: ') +
272+ ','.join([p.name for p in non_public_products]))
273+
274+ if self.account_status != AccountStatus.ACTIVE:
275+ errors.append('This account is already deactivated.')
276+
277+ return (not errors), errors
278+
279 # XXX: salgado, 2009-04-16: This should be called just deactivate(),
280 # because it not only deactivates this person's account but also the
281 # person.
282- def deactivateAccount(self, comment):
283+ def deactivateAccount(self, comment, can_deactivate=None):
284 """See `IPersonSpecialRestricted`."""
285 if not self.is_valid_person:
286 raise AssertionError(
287 "You can only deactivate an account of a valid person.")
288
289+ if can_deactivate is None:
290+ # The person can only be deactivated if they do not own any
291+ # non-public products.
292+ can_deactivate = self.canDeactivateAccount()
293+
294+ if not can_deactivate:
295+ message = ("You cannot deactivate an account that owns a "
296+ "non-public product.")
297+ raise AssertionError(message)
298+
299 for membership in self.team_memberships:
300 self.leave(membership.team)
301
302@@ -2146,7 +2230,7 @@
303 registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
304 for team in Person.selectBy(teamowner=self):
305 team.teamowner = registry_experts
306- for pillar_name in self.getAffiliatedPillars():
307+ for pillar_name in self.getAffiliatedPillars(self):
308 pillar = pillar_name.pillar
309 # XXX flacoste 2007-11-26 bug=164635 The comparison using id below
310 # works around a nasty intermittent failure.
311@@ -2155,9 +2239,16 @@
312 pillar.owner = registry_experts
313 changed = True
314 if pillar.driver is not None and pillar.driver.id == self.id:
315- pillar.driver = registry_experts
316+ pillar.driver = None
317 changed = True
318
319+ # Products need to change the bug supervisor as well.
320+ if IProduct.providedBy(pillar):
321+ if (pillar.bug_supervisor is not None and
322+ pillar.bug_supervisor.id == self.id):
323+ pillar.bug_supervisor = None
324+ changed = True
325+
326 if not changed:
327 # Since we removed the person from all teams, something is
328 # seriously broken here.
329
330=== modified file 'lib/lp/registry/model/product.py'
331--- lib/lp/registry/model/product.py 2012-10-22 02:30:44 +0000
332+++ lib/lp/registry/model/product.py 2012-10-24 09:24:22 +0000
333@@ -1735,6 +1735,15 @@
334 Product.id.is_in(Select(Product.id, granted_products)))
335
336 @classmethod
337+ def get_users_private_products(cls, user):
338+ """List the non-public products the user owns."""
339+ result = IStore(Product).find(
340+ Product,
341+ Product._owner == user,
342+ Product._information_type.is_in(PROPRIETARY_INFORMATION_TYPES))
343+ return result
344+
345+ @classmethod
346 def get_all_active(cls, user, eager_load=True):
347 clause = cls.getProductPrivacyFilter(user)
348 result = IStore(Product).find(Product, Product.active,
349
350=== modified file 'lib/lp/registry/tests/test_person.py'
351--- lib/lp/registry/tests/test_person.py 2012-10-17 15:16:09 +0000
352+++ lib/lp/registry/tests/test_person.py 2012-10-24 09:24:22 +0000
353@@ -336,7 +336,7 @@
354 expected_pillars = [
355 distribution.name, project_group.name, project.name]
356 received_pillars = [
357- pillar.name for pillar in user.getAffiliatedPillars()]
358+ pillar.name for pillar in user.getAffiliatedPillars(user)]
359 self.assertEqual(expected_pillars, received_pillars)
360
361 def test_getAffiliatedPillars_roles(self):
362@@ -352,7 +352,7 @@
363 expected_pillars = [
364 driven_project.name, owned_project.name, supervised_project.name]
365 received_pillars = [
366- pillar.name for pillar in user.getAffiliatedPillars()]
367+ pillar.name for pillar in user.getAffiliatedPillars(user)]
368 self.assertEqual(expected_pillars, received_pillars)
369
370 def test_getAffiliatedPillars_active_pillars(self):
371@@ -364,7 +364,76 @@
372 inactive_project.active = False
373 expected_pillars = [active_project.name]
374 received_pillars = [pillar.name for pillar in
375- user.getAffiliatedPillars()]
376+ user.getAffiliatedPillars(user)]
377+ self.assertEqual(expected_pillars, received_pillars)
378+
379+ def test_getAffiliatedPillars_minus_embargoed(self):
380+ # Skip non public products if not allowed to see them.
381+ owner = self.factory.makePerson()
382+ user = self.factory.makePerson()
383+ self.factory.makeProduct(
384+ information_type=InformationType.EMBARGOED,
385+ owner=owner)
386+ public = self.factory.makeProduct(
387+ information_type=InformationType.PUBLIC,
388+ owner=owner)
389+
390+ expected_pillars = [public.name]
391+ received_pillars = [pillar.name for pillar in
392+ owner.getAffiliatedPillars(user)]
393+ self.assertEqual(expected_pillars, received_pillars)
394+
395+ def test_getAffiliatedPillars_visible_to_self(self):
396+ # Users can see their own non-public affiliated products.
397+ owner = self.factory.makePerson()
398+ self.factory.makeProduct(
399+ name=u'embargoed',
400+ information_type=InformationType.EMBARGOED,
401+ owner=owner)
402+ self.factory.makeProduct(
403+ name=u'public',
404+ information_type=InformationType.PUBLIC,
405+ owner=owner)
406+
407+ expected_pillars = [u'embargoed', u'public']
408+ received_pillars = [pillar.name for pillar in
409+ owner.getAffiliatedPillars(owner)]
410+ self.assertEqual(expected_pillars, received_pillars)
411+
412+ def test_getAffiliatedPillars_visible_to_admins(self):
413+ # Users can see their own non-public affiliated products.
414+ owner = self.factory.makePerson()
415+ admin = self.factory.makeAdministrator()
416+ self.factory.makeProduct(
417+ name=u'embargoed',
418+ information_type=InformationType.EMBARGOED,
419+ owner=owner)
420+ self.factory.makeProduct(
421+ name=u'public',
422+ information_type=InformationType.PUBLIC,
423+ owner=owner)
424+
425+ expected_pillars = [u'embargoed', u'public']
426+ received_pillars = [pillar.name for pillar in
427+ owner.getAffiliatedPillars(admin)]
428+ self.assertEqual(expected_pillars, received_pillars)
429+
430+ def test_getAffiliatedPillars_visible_to_commercial_admins(self):
431+ # Users can see their own non-public affiliated products.
432+ owner = self.factory.makePerson()
433+ admin = self.factory.makeCommercialAdmin()
434+ self.factory.makeProduct(
435+ name=u'embargoed',
436+ information_type=InformationType.EMBARGOED,
437+ owner=owner)
438+ self.factory.makeProduct(
439+ name=u'public',
440+ information_type=InformationType.PUBLIC,
441+ owner=owner)
442+
443+ expected_pillars = [u'embargoed', u'public']
444+ received_pillars = [pillar.name for pillar in
445+ owner.getAffiliatedPillars(admin)]
446 self.assertEqual(expected_pillars, received_pillars)
447
448 def test_no_merge_pending(self):
449@@ -675,6 +744,26 @@
450 self.bzr = product_set.getByName('bzr')
451 self.now = datetime.now(pytz.UTC)
452
453+ def test_canDeactivateAccount_private_projects(self):
454+ """A user owning non-public products cannot be deactivated."""
455+ user = self.factory.makePerson()
456+ public_product = self.factory.makeProduct(
457+ information_type=InformationType.PUBLIC,
458+ name="public",
459+ owner=user)
460+ public_product = self.factory.makeProduct(
461+ information_type=InformationType.PROPRIETARY,
462+ name="private",
463+ owner=user)
464+
465+ login(user.preferredemail.email)
466+ can_deactivate, errors = user.canDeactivateAccountWithErrors()
467+
468+ self.assertFalse(can_deactivate)
469+ expected_error = ('This account cannot be deactivated because it owns '
470+ 'the following non-public products: private')
471+ self.assertIn(expected_error, errors)
472+
473 def test_deactivateAccount_copes_with_names_already_in_use(self):
474 """When a user deactivates his account, its name is changed.
475
476@@ -710,12 +799,15 @@
477 product = self.factory.makeProduct(owner=user)
478 with person_logged_in(user):
479 product.driver = user
480+ product.bug_supervisor = user
481 user.deactivateAccount("Going off the grid.")
482 registry_team = getUtility(ILaunchpadCelebrities).registry_experts
483 self.assertEqual(registry_team, product.owner,
484 "Owner is not registry team.")
485- self.assertEqual(registry_team, product.driver,
486- "Driver is not registry team.")
487+ self.assertEqual(None, product.driver,
488+ "Driver is not emptied.")
489+ self.assertEqual(None, product.bug_supervisor,
490+ "Driver is not emptied.")
491
492 def test_getDirectMemberIParticipateIn(self):
493 sample_person = Person.byName('name12')
494
495=== modified file 'lib/lp/registry/tests/test_product.py'
496--- lib/lp/registry/tests/test_product.py 2012-10-18 15:01:49 +0000
497+++ lib/lp/registry/tests/test_product.py 2012-10-24 09:24:22 +0000
498@@ -1866,6 +1866,22 @@
499 clause = ProductSet.getProductPrivacyFilter(user)
500 return IStore(Product).find(Product, clause)
501
502+ def test_users_private_products(self):
503+ # Ignore any public products the user may own.
504+ owner = self.factory.makePerson()
505+ public = self.factory.makeProduct(
506+ information_type=InformationType.PUBLIC,
507+ owner=owner)
508+ proprietary = self.factory.makeProduct(
509+ information_type=InformationType.PROPRIETARY,
510+ owner=owner)
511+ embargoed = self.factory.makeProduct(
512+ information_type=InformationType.EMBARGOED,
513+ owner=owner)
514+ result = ProductSet.get_users_private_products(owner)
515+ self.assertIn(proprietary, result)
516+ self.assertIn(embargoed, result)
517+
518 def test_get_all_active_omits_proprietary(self):
519 # Ignore proprietary products for anonymous users
520 proprietary = self.factory.makeProduct(