Merge lp://staging/~bac/launchpad/bug-410416 into lp://staging/launchpad

Proposed by Brad Crittenden
Status: Merged
Merged at revision: not available
Proposed branch: lp://staging/~bac/launchpad/bug-410416
Merge into: lp://staging/launchpad
Diff against target: None lines
To merge this branch: bzr merge lp://staging/~bac/launchpad/bug-410416
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+9932@code.staging.launchpad.net
To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

= Summary =

An oversight was discovered in that private teams can not play the role of project
owner or driver, which was filed as Bug 410416.

In the process of making the fix it was discovered that creating a project also makes
the trunk productseries with the same owner. So productseries owner and driver were
also changed to allow private teams.

== Proposed fix ==

Change the type and validators for those properties to allow private teams.

== Pre-implementation notes ==

None.

== Implementation details ==

As above.

== Tests ==

bin/test -vvm lp.registry -t private-team-roles.txt

== Demo and Q/A ==

QA would be to create a private team on staging and ensure can become a project owner
and driver. Same for a product series.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/model/productseries.py
  lib/lp/registry/doc/private-team-roles.txt
  lib/lp/registry/interfaces/product.py
  lib/lp/registry/interfaces/productseries.py
  lib/lp/registry/model/product.py

== Pyflakes notices ==

lib/lp/registry/model/productseries.py
    37: 'validate_public_person' imported but unused

== Pylint notices ==

lib/lp/registry/model/productseries.py
    37: [W0611] Unused import validate_public_person

This import will be removed before landing.

Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (3.4 KiB)

Hi Brad. Thank you for this excellent test. It does a lot to clarify what private teams
can do. I have some trivial suggesting to improve this branch.

I have a question about the very part of the test. Since you are just documenting
existing behaviour in this case, I do not think we need to change anything at this time.

> === modified file 'lib/lp/registry/doc/private-team-roles.txt'
> --- lib/lp/registry/doc/private-team-roles.txt 2009-08-03 01:41:07 +0000
> +++ lib/lp/registry/doc/private-team-roles.txt 2009-08-10 17:08:27 +0000

...

Use two leading blank lines to separate a heading from the previous content.

> +Maintainer/Owner
> +----------------
> +
> +A public team and a private team can be a project owner but a private membership team cannot.

Wrap the narrative at 78 characters.

> +
> + >>> # The registrant must be specified or it will default to the owner.
> + >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)
> + >>> product = factory.makeProduct(registrant=admin_user, owner=priv_team)
> +
> + >>> product = factory.makeProduct(registrant=admin_user, owner=pm_team)
> + Traceback (most recent call last):
> + ...
> + PrivatePersonLinkageError: Cannot link person
> + (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
> + <Product at...
> +
> +
> +Driver
> +------
> +
> +A public team and a private team can be a project driver but a private membership team cannot.

Wrap the narrative at 78 characters.

...

> + <Product at...
> +
> +Product Series Roles
> +====================

Use two leading blank lines to separate a heading from the previous content.

> +Owner
> +-----
> +
> +A public team and a private team can be a product series owner but a
> +private membership team cannot.
> +
> + >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)

Is this line 79 characters?

...

> +Driver
> +------
> +
> +A public team and a private team can be a product series driver but a
> +private membership team cannot.
> +
> + >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)

Is this line 79 characters?

...

> +Team Membership
> +===============
> +
> +Mixing public and private teams can create interesting situations.
> +Not every type of team can become a member of other types.
> +
> + >>> from lp.registry.interfaces.person import PrivatePersonLinkageError
> + >>> reviewer = factory.makePerson()
> + >>> def join_team(joined_type, joiner_type):
> + ... joined = factory.makeTeam(owner=team_owner, visibility=joined_type)
> + ... joiner = factory.makeTeam(owner=team_owner, visibility=joiner_type)

Wrap the code at 78 characters.

...

> + Public <- Public: Allowed
> + Public <- Private Membership: Not Allowed
> + Public <- Private: Not Allowed
> + ---
> + Private Membership <- Public: Allowed
> + Private Membership <- Private Membership: Not Allowed
> + Private Membership <- Private: Not Allowed
> + ---
> + Private <- Public: Allowed

Really? Is my team really private if the owner of the public team adds every
team he can find on launchpad? Is there some other guard in p...

Read more...

review: Needs Fixing (code)
Revision history for this message
Brad Crittenden (bac) wrote :

> Hi Brad. Thank you for this excellent test. It does a lot to clarify what
> private teams
> can do. I have some trivial suggesting to improve this branch.
>
> I have a question about the very part of the test. Since you are just
> documenting
> existing behaviour in this case, I do not think we need to change anything at
> this time.
>
> > === modified file 'lib/lp/registry/doc/private-team-roles.txt'
> > --- lib/lp/registry/doc/private-team-roles.txt 2009-08-03 01:41:07
> +0000
> > +++ lib/lp/registry/doc/private-team-roles.txt 2009-08-10 17:08:27
> +0000
>
> ...
>
> Use two leading blank lines to separate a heading from the previous content.

Thanks for finding the formatting mistakes. They have all been corrected.

> ...
>
> > + Public <- Public: Allowed
> > + Public <- Private Membership: Not Allowed
> > + Public <- Private: Not Allowed
> > + ---
> > + Private Membership <- Public: Allowed
> > + Private Membership <- Private Membership: Not Allowed
> > + Private Membership <- Private: Not Allowed
> > + ---
> > + Private <- Public: Allowed
>
> Really? Is my team really private if the owner of the public team adds every
> team he can find on launchpad? Is there some other guard in place to prevent
> a the public team from exposing the private? A waring to the private team
> that is is risking exposure by adding a public team that it does not control?

As you said, this branch is about expanding the roles of private teams and documenting what they can and cannot do, including the current rules for team membership.

I was also surprised to see that public teams can join private teams. Or, more precisely, private teams can add a public team as a member.

If the owner of a private team willingly adds a public team to it then there are no additional safeguards. The members of the public sub-team can see into the private team's membership. The public team does not become a vector for non-members to see into the private team, however.

Adding a public team with restricted membership may have some valid use cases. Adding a public team with open membership is a disaster in waiting.

These issues are outside the scope of this branch but need to be addressed soon.

>
> > + Private <- Private Membership: Not Allowed
> > + Private <- Private: Not Allowed
> > + ---

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Brad.

Thank you for the explanation of the private <- public team relationship. We can discuss issue later. This branch is fine to land.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/doc/private-team-roles.txt'
--- lib/lp/registry/doc/private-team-roles.txt 2009-08-03 01:41:07 +0000
+++ lib/lp/registry/doc/private-team-roles.txt 2009-08-10 17:08:27 +0000
@@ -292,3 +292,162 @@
292 PrivatePersonLinkageError: Cannot link person292 PrivatePersonLinkageError: Cannot link person
293 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to293 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
294 <...StructuralSubscription at...294 <...StructuralSubscription at...
295
296
297Project Roles
298=============
299
300Registrant
301----------
302
303Only a person can register a project, not a team, so no team, public
304or private, can be the project registrant.
305
306 >>> login_person(admin_user)
307 >>> public_team = factory.makeTeam(name='public-team',
308 ... owner=team_owner,
309 ... visibility=PersonVisibility.PUBLIC)
310 >>> product = factory.makeProduct(registrant=team_owner)
311 >>> product = factory.makeProduct(registrant=public_team)
312 >>> product = factory.makeProduct(registrant=priv_team)
313 Traceback (most recent call last):
314 ...
315 PrivatePersonLinkageError: Cannot link person
316 (name=private-team, visibility=PRIVATE) to
317 <Product at...
318
319 >>> product = factory.makeProduct(registrant=pm_team)
320 Traceback (most recent call last):
321 ...
322 PrivatePersonLinkageError: Cannot link person
323 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
324 <Product at...
325
326Maintainer/Owner
327----------------
328
329A public team and a private team can be a project owner but a private membership team cannot.
330
331 >>> # The registrant must be specified or it will default to the owner.
332 >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)
333 >>> product = factory.makeProduct(registrant=admin_user, owner=priv_team)
334
335 >>> product = factory.makeProduct(registrant=admin_user, owner=pm_team)
336 Traceback (most recent call last):
337 ...
338 PrivatePersonLinkageError: Cannot link person
339 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
340 <Product at...
341
342
343Driver
344------
345
346A public team and a private team can be a project driver but a private membership team cannot.
347
348 >>> product = factory.makeProduct()
349 >>> product.driver = priv_team
350
351 >>> product.driver = pm_team
352 Traceback (most recent call last):
353 ...
354 PrivatePersonLinkageError: Cannot link person
355 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
356 <Product at...
357
358
359Bug Supervisor
360--------------
361
362A public team and a private team can be a project bug supervisor but a
363private membership team cannot.
364
365 >>> product = factory.makeProduct()
366 >>> product.setBugSupervisor(public_team, admin_user)
367 >>> product.setBugSupervisor(priv_team, admin_user)
368 >>> product.setBugSupervisor(pm_team, admin_user)
369 Traceback (most recent call last):
370 ...
371 PrivatePersonLinkageError: Cannot link person
372 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
373 <Product at...
374
375Product Series Roles
376====================
377
378Owner
379-----
380
381A public team and a private team can be a product series owner but a
382private membership team cannot.
383
384 >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)
385 >>> product_series = factory.makeProductSeries(product, owner=public_team)
386 >>> product_series = factory.makeProductSeries(product, owner=priv_team)
387
388 >>> product_series = factory.makeProductSeries(product, owner=pm_team)
389 Traceback (most recent call last):
390 ...
391 PrivatePersonLinkageError: Cannot link person
392 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
393 <ProductSeries at...
394
395
396Driver
397------
398
399A public team and a private team can be a product series driver but a
400private membership team cannot.
401
402 >>> product = factory.makeProduct(registrant=admin_user, owner=public_team)
403 >>> product_series = factory.makeProductSeries(product, owner=public_team)
404 >>> product_series.driver = public_team
405 >>> product_series.driver = priv_team
406 >>> product_series.driver = pm_team
407 Traceback (most recent call last):
408 ...
409 PrivatePersonLinkageError: Cannot link person
410 (name=private-membership-team, visibility=PRIVATE_MEMBERSHIP) to
411 <ProductSeries at...
412
413
414Team Membership
415===============
416
417Mixing public and private teams can create interesting situations.
418Not every type of team can become a member of other types.
419
420 >>> from lp.registry.interfaces.person import PrivatePersonLinkageError
421 >>> reviewer = factory.makePerson()
422 >>> def join_team(joined_type, joiner_type):
423 ... joined = factory.makeTeam(owner=team_owner, visibility=joined_type)
424 ... joiner = factory.makeTeam(owner=team_owner, visibility=joiner_type)
425 ... print "%s <- %s: " % (joined_type, joiner_type),
426 ... try:
427 ... joined.addMember(joiner, reviewer=reviewer)
428 ... except PrivatePersonLinkageError:
429 ... print "Not Allowed"
430 ... else:
431 ... print "Allowed"
432
433 >>> public = PersonVisibility.PUBLIC
434 >>> private = PersonVisibility.PRIVATE
435 >>> private_membership = PersonVisibility.PRIVATE_MEMBERSHIP
436
437 >>> visibility_list = [public, private, private_membership]
438 >>> for joined in PersonVisibility.items:
439 ... for joiner in PersonVisibility.items:
440 ... join_team(joined, joiner)
441 ... print "---"
442 Public <- Public: Allowed
443 Public <- Private Membership: Not Allowed
444 Public <- Private: Not Allowed
445 ---
446 Private Membership <- Public: Allowed
447 Private Membership <- Private Membership: Not Allowed
448 Private Membership <- Private: Not Allowed
449 ---
450 Private <- Public: Allowed
451 Private <- Private Membership: Not Allowed
452 Private <- Private: Not Allowed
453 ---
295454
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/interfaces/product.py 2009-08-10 17:08:27 +0000
@@ -37,8 +37,8 @@
37from canonical.launchpad import _37from canonical.launchpad import _
38from canonical.launchpad.fields import (38from canonical.launchpad.fields import (
39 Description, IconImageUpload, LogoImageUpload, MugshotImageUpload,39 Description, IconImageUpload, LogoImageUpload, MugshotImageUpload,
40 ProductBugTracker, ProductNameField, PublicPersonChoice,40 ParticipatingPersonChoice, ProductBugTracker, ProductNameField,
41 Summary, Title, URIField)41 PublicPersonChoice, Summary, Title, URIField)
42from lp.code.interfaces.branchvisibilitypolicy import (42from lp.code.interfaces.branchvisibilitypolicy import (
43 IHasBranchVisibilityPolicy)43 IHasBranchVisibilityPolicy)
44from lp.code.interfaces.hasbranches import IHasBranches, IHasMergeProposals44from lp.code.interfaces.hasbranches import IHasBranches, IHasMergeProposals
@@ -353,7 +353,7 @@
353 exported_as='project_group')353 exported_as='project_group')
354354
355 owner = exported(355 owner = exported(
356 PublicPersonChoice(356 ParticipatingPersonChoice(
357 title=_('Maintainer'),357 title=_('Maintainer'),
358 required=True,358 required=True,
359 vocabulary='ValidOwner',359 vocabulary='ValidOwner',
@@ -370,7 +370,7 @@
370 "Launchpad.")))370 "Launchpad.")))
371371
372 driver = exported(372 driver = exported(
373 PublicPersonChoice(373 ParticipatingPersonChoice(
374 title=_("Driver"),374 title=_("Driver"),
375 description=_(375 description=_(
376 "This person or team will be able to set feature goals for "376 "This person or team will be able to set feature goals for "
377377
=== modified file 'lib/lp/registry/interfaces/productseries.py'
--- lib/lp/registry/interfaces/productseries.py 2009-07-19 04:41:14 +0000
+++ lib/lp/registry/interfaces/productseries.py 2009-08-10 17:08:27 +0000
@@ -19,7 +19,8 @@
19from zope.interface import Interface, Attribute19from zope.interface import Interface, Attribute
2020
21from canonical.launchpad.fields import (21from canonical.launchpad.fields import (
22 ContentNameField, NoneableDescription, PublicPersonChoice, Title)22 ContentNameField, NoneableDescription, ParticipatingPersonChoice,
23 PublicPersonChoice, Title)
23from lp.code.interfaces.branch import IBranch24from lp.code.interfaces.branch import IBranch
24from lp.bugs.interfaces.bugtarget import IBugTarget25from lp.bugs.interfaces.bugtarget import IBugTarget
25from lp.registry.interfaces.distroseries import DistroSeriesStatus26from lp.registry.interfaces.distroseries import DistroSeriesStatus
@@ -125,12 +126,12 @@
125 exported_as='date_created')126 exported_as='date_created')
126127
127 owner = exported(128 owner = exported(
128 PublicPersonChoice(129 ParticipatingPersonChoice(
129 title=_('Owner'), required=True, vocabulary='ValidOwner',130 title=_('Owner'), required=True, vocabulary='ValidOwner',
130 description=_('Project owner, either a valid Person or Team')))131 description=_('Project owner, either a valid Person or Team')))
131132
132 driver = exported(133 driver = exported(
133 PublicPersonChoice(134 ParticipatingPersonChoice(
134 title=_("Release manager"),135 title=_("Release manager"),
135 description=_(136 description=_(
136 "The person or team responsible for decisions about features "137 "The person or team responsible for decisions about features "
137138
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2009-07-19 04:41:14 +0000
+++ lib/lp/registry/model/product.py 2009-08-10 17:08:27 +0000
@@ -183,11 +183,12 @@
183 project = ForeignKey(183 project = ForeignKey(
184 foreignKey="Project", dbName="project", notNull=False, default=None)184 foreignKey="Project", dbName="project", notNull=False, default=None)
185 owner = ForeignKey(185 owner = ForeignKey(
186 foreignKey="Person",186 dbName="owner", foreignKey="Person",
187 storm_validator=validate_public_person, dbName="owner", notNull=True)187 storm_validator=validate_person_not_private_membership,
188 notNull=True)
188 registrant = ForeignKey(189 registrant = ForeignKey(
189 foreignKey="Person",190 dbName="registrant", foreignKey="Person",
190 storm_validator=validate_public_person, dbName="registrant",191 storm_validator=validate_public_person,
191 notNull=True)192 notNull=True)
192 bug_supervisor = ForeignKey(193 bug_supervisor = ForeignKey(
193 dbName='bug_supervisor', foreignKey='Person',194 dbName='bug_supervisor', foreignKey='Person',
@@ -200,7 +201,8 @@
200 default=None)201 default=None)
201 driver = ForeignKey(202 driver = ForeignKey(
202 dbName="driver", foreignKey="Person",203 dbName="driver", foreignKey="Person",
203 storm_validator=validate_public_person, notNull=False, default=None)204 storm_validator=validate_person_not_private_membership,
205 notNull=False, default=None)
204 name = StringCol(206 name = StringCol(
205 dbName='name', notNull=True, alternateID=True, unique=True)207 dbName='name', notNull=True, alternateID=True, unique=True)
206 displayname = StringCol(dbName='displayname', notNull=True)208 displayname = StringCol(dbName='displayname', notNull=True)
207209
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2009-07-17 00:26:05 +0000
+++ lib/lp/registry/model/productseries.py 2009-08-10 17:08:27 +0000
@@ -34,7 +34,8 @@
34from lp.registry.model.milestone import (34from lp.registry.model.milestone import (
35 HasMilestonesMixin, Milestone)35 HasMilestonesMixin, Milestone)
36from canonical.launchpad.database.packaging import Packaging36from canonical.launchpad.database.packaging import Packaging
37from lp.registry.interfaces.person import validate_public_person37from lp.registry.interfaces.person import (
38 validate_person_not_private_membership, validate_public_person)
38from lp.translations.model.pofile import POFile39from lp.translations.model.pofile import POFile
39from lp.translations.model.potemplate import (40from lp.translations.model.potemplate import (
40 HasTranslationTemplatesMixin,41 HasTranslationTemplatesMixin,
@@ -101,10 +102,12 @@
101 datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)102 datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
102 owner = ForeignKey(103 owner = ForeignKey(
103 dbName="owner", foreignKey="Person",104 dbName="owner", foreignKey="Person",
104 storm_validator=validate_public_person, notNull=True)105 storm_validator=validate_person_not_private_membership,
106 notNull=True)
105 driver = ForeignKey(107 driver = ForeignKey(
106 dbName="driver", foreignKey="Person",108 dbName="driver", foreignKey="Person",
107 storm_validator=validate_public_person, notNull=False, default=None)109 storm_validator=validate_person_not_private_membership,
110 notNull=False, default=None)
108 branch = ForeignKey(foreignKey='Branch', dbName='branch',111 branch = ForeignKey(foreignKey='Branch', dbName='branch',
109 default=None)112 default=None)
110 translations_autoimport_mode = EnumCol(113 translations_autoimport_mode = EnumCol(