Merge lp:~edwin-grubbs/launchpad/bug-506965-merge-team-oops into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Curtis Hovey
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~edwin-grubbs/launchpad/bug-506965-merge-team-oops
Merge into: lp:launchpad
Diff against target: 246 lines (+82/-43)
4 files modified
lib/lp/registry/browser/menu.py (+1/-1)
lib/lp/registry/browser/peoplemerge.py (+4/-4)
lib/lp/registry/browser/tests/peoplemerge-views.txt (+34/-4)
lib/lp/registry/stories/team/xx-adminteammerge.txt (+43/-34)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-506965-merge-team-oops
Reviewer Review Type Date Requested Status
Curtis Hovey (community) Approve
Review via email: mp+17445@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Summary
-------

Fixed oops caused when a registry expert (non-admin) merges a team with an email address into another team.

Implementation details
----------------------

Set email status after removing security adapter to give the registry expert access in this special case.

Tests
-----

./bin/test -vv -t xx-adminteammerge.txt

Demo and Q/A
------------

* Add ~no-priv to the ~registry team.
* Login as ~no-priv.
* Open http://launchpad.dev/people/+newteam
  * Add a team with a contact email address.
  * You will need to verify the email address with the token it emails you. (On launchpad.dev, you can just look in the logintoken table and paste the token into https://launchpad.dev/token/$token)
* Open http://launchpad.dev/people/
  * Click on "Merge teams"
  * Enter you new team for the first team.
  * Enter "ubuntu-team" for the second team.
  * Click "Merge"
  * When you get a warning message, click "Deactivate members and merge".

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

Hi Edwin.

There are some dangerous changes in the code and the test does is not what is happening in production.

We need to revert the permissions on the view. It must be admin because we do not want every team owner/admin permission to merge their team with *any* other team. Delete restricts the operation to the context team and ~registry.

> Demo and Q/A
> ------------
>
> * Add ~no-priv to the ~registry team.
> * Login as ~no-priv.
> * Open http://launchpad.dev/people/+newteam
> * Add a team with a contact email address.
> * You will need to verify the email address with the token it emails you.
> (On launchpad.dev, you can just look in the logintoken table and paste the
> token into https://launchpad.dev/token/$token)
> * Open http://launchpad.dev/people/
> * Click on "Merge teams"
> * Enter you new team for the first team.
> * Enter "ubuntu-team" for the second team.
> * Click "Merge"
> * When you get a warning message, click "Deactivate members and merge".

This story is wrong. The delete team scenario is a very restricted merge operation that registry administators may perform. Admins can do unrestricted merges.
    * As registry expert.
    * Visit https://edge.launchpad.net/~consulvision
      * This team has an email address in NEW status
      * team email addresses are not shown in the UI
      * only the team owner can access the address.
    * Choose Delete team.
    * Verify the team is gone.

The code change is the the view, so we want a test that verifies the view handles the real conditions. The test should be in lp/registry/tests/peoplemerge-views.txt. You can update the the Delete team section to cover this story
    * Create a team with an email address.
    * deletable_.setContactAddress(None)
    * login_person(registry_experts.teamowner)
    * form = {'field.actions.delete': 'Delete'}
    * view = create_initialized_view(deletable_team, '+delete', form=form)
    * view.errors
      []
    * for notification in view.request.response.notifications:
         print notification.message
      Team deleted.

review: Needs Fixing (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

> * Create a team with an email address.
> * deletable_.setContactAddress(None)

I think I meant deletable_team.

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (5.8 KiB)

> Hi Edwin.
>
> There are some dangerous changes in the code and the test does is not what is
> happening in production.
>
> We need to revert the permissions on the view. It must be admin because we do
> not want every team owner/admin permission to merge their team with *any*
> other team. Delete restricts the operation to the context team and ~registry.

Hi Sinzui,

Thanks for the review. As discussed on IRC, the permissions for the view and for the +adminmergeteam link are fine since it uses launchpad.Moderate on IPersonSet, which does not include admins of any team.

> > Demo and Q/A
> > ------------
> >
> > * Add ~no-priv to the ~registry team.
> > * Login as ~no-priv.
> > * Open http://launchpad.dev/people/+newteam
> > * Add a team with a contact email address.
> > * You will need to verify the email address with the token it emails you.
> > (On launchpad.dev, you can just look in the logintoken table and paste the
> > token into https://launchpad.dev/token/$token)
> > * Open http://launchpad.dev/people/
> > * Click on "Merge teams"
> > * Enter you new team for the first team.
> > * Enter "ubuntu-team" for the second team.
> > * Click "Merge"
> > * When you get a warning message, click "Deactivate members and merge".
>
> This story is wrong. The delete team scenario is a very restricted merge
> operation that registry administators may perform. Admins can do unrestricted
> merges.
> * As registry expert.
> * Visit https://edge.launchpad.net/~consulvision
> * This team has an email address in NEW status
> * team email addresses are not shown in the UI
> * only the team owner can access the address.
> * Choose Delete team.
> * Verify the team is gone.
>
> The code change is the the view, so we want a test that verifies the view
> handles the real conditions. The test should be in lp/registry/tests
> /peoplemerge-views.txt. You can update the the Delete team section to cover
> this story
> * Create a team with an email address.
> * deletable_.setContactAddress(None)
> * login_person(registry_experts.teamowner)
> * form = {'field.actions.delete': 'Delete'}
> * view = create_initialized_view(deletable_team, '+delete', form=form)
> * view.errors
> []
> * for notification in view.request.response.notifications:
> print notification.message
> Team deleted.

I have added this test. I also changed other parts of the test that was using the owner of the registry team, which is mark. Since mark is also in the admin team, I don't think it is a very effective test of the registry experts abilities to use him.

-Edwin

Incremental diff:
{{{
=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
--- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-06 17:52:45 +0000
+++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-19 23:53:42 +0000
@@ -6,9 +6,17 @@
 Team Merges
 -----------

+Create a member of the registry team that is not a member of the admins
+team.
+
     >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
+ >>> registry_e...

Read more...

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

Hi Edwin.

Thank you for correcting the registry-experts test. I am sure something
bad would have happened using a super-powerful moderator. This did
happen to me in a test for mirror admins, which user Mark owns.

I have one small suggestion for the test.

  review approve

On Wed, 2010-01-20 at 00:02 +0000, Edwin Grubbs wrote:
> Incremental diff:
> {{{
> === modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
> --- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-06 17:52:45 +0000
> +++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-19 23:53:42 +0000
> @@ -6,9 +6,17 @@
> Team Merges
> -----------
...

> @@ -39,6 +47,7 @@
> >>> parent_team = factory.makeTeam()
> >>> child_team = factory.makeTeam(name='child-team')
> >>> random_team = factory.makeTeam()
> + >>> login('<email address hidden>')

Use login('<email address hidden>') so that the role is clear.

...

--
__Curtis C. Hovey_________
http://launchpad.net/

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/menu.py'
--- lib/lp/registry/browser/menu.py 2009-08-21 20:04:25 +0000
+++ lib/lp/registry/browser/menu.py 2010-01-20 00:01:15 +0000
@@ -60,7 +60,7 @@
60 text = 'Merge people'60 text = 'Merge people'
61 return Link('/people/+adminpeoplemerge', text, icon='edit')61 return Link('/people/+adminpeoplemerge', text, icon='edit')
6262
63 @enabled_with_permission('launchpad.Admin')63 @enabled_with_permission('launchpad.Moderate')
64 def admin_merge_teams(self):64 def admin_merge_teams(self):
65 text = 'Merge teams'65 text = 'Merge teams'
66 return Link('/people/+adminteammerge', text, icon='edit')66 return Link('/people/+adminteammerge', text, icon='edit')
6767
=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py 2010-01-12 00:19:20 +0000
+++ lib/lp/registry/browser/peoplemerge.py 2010-01-20 00:01:15 +0000
@@ -140,15 +140,15 @@
140 from zope.security.proxy import removeSecurityProxy140 from zope.security.proxy import removeSecurityProxy
141 for email in self.dupe_person_emails:141 for email in self.dupe_person_emails:
142 email = IMasterObject(email)142 email = IMasterObject(email)
143 # XXX: Guilherme Salgado 2007-10-15: Maybe this status change
144 # should be done only when merging people but not when merging
145 # teams.
146 email.status = EmailAddressStatus.NEW
147 # EmailAddress.person and EmailAddress.account are readonly143 # EmailAddress.person and EmailAddress.account are readonly
148 # fields, so we need to remove the security proxy here.144 # fields, so we need to remove the security proxy here.
149 naked_email = removeSecurityProxy(email)145 naked_email = removeSecurityProxy(email)
150 naked_email.personID = self.target_person.id146 naked_email.personID = self.target_person.id
151 naked_email.accountID = self.target_person.accountID147 naked_email.accountID = self.target_person.accountID
148 # XXX: Guilherme Salgado 2007-10-15: Maybe this status change
149 # should be done only when merging people but not when merging
150 # teams.
151 naked_email.status = EmailAddressStatus.NEW
152 flush_database_updates()152 flush_database_updates()
153 getUtility(IPersonSet).merge(self.dupe_person, self.target_person)153 getUtility(IPersonSet).merge(self.dupe_person, self.target_person)
154 self.request.response.addInfoNotification(self.merge_message)154 self.request.response.addInfoNotification(self.merge_message)
155155
=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
--- lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-06 17:52:45 +0000
+++ lib/lp/registry/browser/tests/peoplemerge-views.txt 2010-01-20 00:01:15 +0000
@@ -6,9 +6,17 @@
6Team Merges6Team Merges
7-----------7-----------
88
9Create a member of the registry team that is not a member of the admins
10team.
11
9 >>> from lp.registry.interfaces.person import IPersonSet12 >>> from lp.registry.interfaces.person import IPersonSet
13 >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
14 >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
10 >>> person_set = getUtility(IPersonSet)15 >>> person_set = getUtility(IPersonSet)
11 >>> registry_expert = person_set.getByName('mark')16 >>> registry_expert = factory.makePerson()
17 >>> login_person(registry_experts.teamowner)
18 >>> ignored = registry_experts.addMember(
19 ... registry_expert, registry_experts.teamowner)
12 >>> login_person(registry_expert)20 >>> login_person(registry_expert)
1321
14A team (name21) can be merged into another (ubuntu-team).22A team (name21) can be merged into another (ubuntu-team).
@@ -39,6 +47,7 @@
39 >>> parent_team = factory.makeTeam()47 >>> parent_team = factory.makeTeam()
40 >>> child_team = factory.makeTeam(name='child-team')48 >>> child_team = factory.makeTeam(name='child-team')
41 >>> random_team = factory.makeTeam()49 >>> random_team = factory.makeTeam()
50 >>> login('foo.bar@canonical.com')
42 >>> ignored = parent_team.addMember(51 >>> ignored = parent_team.addMember(
43 ... child_team, reviewer=parent_team.teamowner, force_team_add=True)52 ... child_team, reviewer=parent_team.teamowner, force_team_add=True)
44 >>> form = {'field.dupe_person': child_team.name,53 >>> form = {'field.dupe_person': child_team.name,
@@ -112,10 +121,8 @@
112Users with launchpad.Moderate such as team admins and registry experts121Users with launchpad.Moderate such as team admins and registry experts
113can delete teams.122can delete teams.
114123
115 >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
116 >>> from canonical.launchpad.webapp.authorization import check_permission124 >>> from canonical.launchpad.webapp.authorization import check_permission
117125
118 >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
119 >>> team_owner = factory.makePerson()126 >>> team_owner = factory.makePerson()
120 >>> team_member = factory.makePerson()127 >>> team_member = factory.makePerson()
121 >>> deletable_team = factory.makeTeam(owner=team_owner, name='deletable')128 >>> deletable_team = factory.makeTeam(owner=team_owner, name='deletable')
@@ -127,7 +134,7 @@
127 >>> check_permission('launchpad.Moderate', view)134 >>> check_permission('launchpad.Moderate', view)
128 False135 False
129136
130 >>> login_person(registry_experts.teamowner)137 >>> login_person(registry_expert)
131 >>> check_permission('launchpad.Moderate', view)138 >>> check_permission('launchpad.Moderate', view)
132 True139 True
133140
@@ -241,3 +248,26 @@
241248
242 >>> print find_tag_by_id(content, 'field.actions.delete')249 >>> print find_tag_by_id(content, 'field.actions.delete')
243 None250 None
251
252The registry experts should be able to delete a team with an
253validated email address, which will be invisible, since only
254preferred email addresses are shown for teams.
255
256 >>> from canonical.launchpad.interfaces.emailaddress import (
257 ... IEmailAddressSet)
258 >>> login_person(registry_expert)
259 >>> admin = getUtility(ILaunchpadCelebrities).admin
260 >>> registry_expert.inTeam(admin)
261 False
262 >>> deletable_team = factory.makeTeam(email="del@example.com")
263 >>> deletable_team.setContactAddress(None)
264 >>> for email in getUtility(IEmailAddressSet).getByPerson(deletable_team):
265 ... print email.email, email.status.title
266 del@example.com Validated Email Address
267 >>> form = {'field.actions.delete': 'Delete'}
268 >>> view = create_initialized_view(deletable_team, '+delete', form=form)
269 >>> view.errors
270 []
271 >>> for notification in view.request.response.notifications:
272 ... print notification.message
273 Team deleted.
244274
=== modified file 'lib/lp/registry/stories/team/xx-adminteammerge.txt'
--- lib/lp/registry/stories/team/xx-adminteammerge.txt 2009-11-22 15:43:16 +0000
+++ lib/lp/registry/stories/team/xx-adminteammerge.txt 2010-01-20 00:01:15 +0000
@@ -4,30 +4,39 @@
4active members, so the user will first have to confirm that the4active members, so the user will first have to confirm that the
5members should be deactivated before the teams are merged.5members should be deactivated before the teams are merged.
66
7 >>> admin_browser.open('http://launchpad.dev/people/+adminteammerge')7 >>> from zope.component import getUtility
8 >>> admin_browser.getControl('Duplicated Team').value = (8 >>> from lp.registry.interfaces.person import IPersonSet
9 ... 'landscape-developers')9 >>> login('foo.bar@canonical.com')
10 >>> admin_browser.getControl('Target Team').value = 'guadamen'10 >>> registry_expert = factory.makePerson(
11 >>> admin_browser.getControl('Merge').click()11 ... email='reg@example.com', password='test')
12 >>> new_team = factory.makeTeam(
13 ... name="new-team", email="new_team@example.com")
14 >>> registry_experts = getUtility(IPersonSet).getByName('registry')
15 >>> ignored = registry_experts.addMember(
16 ... registry_expert, registry_experts.teamowner)
17 >>> logout()
18 >>> registry_browser = setupBrowser(auth='Basic reg@example.com:test')
19 >>> registry_browser.open('http://launchpad.dev/people/+adminteammerge')
20 >>> registry_browser.getControl('Duplicated Team').value = (
21 ... 'new-team')
22 >>> registry_browser.getControl('Target Team').value = 'guadamen'
23 >>> registry_browser.getControl('Merge').click()
1224
13 >>> admin_browser.url25 >>> registry_browser.url
14 'http://launchpad.dev/people/+adminteammerge'26 'http://launchpad.dev/people/+adminteammerge'
15 >>> print get_feedback_messages(admin_browser.contents)[0]27 >>> print get_feedback_messages(registry_browser.contents)[0]
16 Landscape Developers has 2 active members which will have to be28 New Team has 1 active members which will have to be deactivated
17 deactivated before the teams can be merged.29 before the teams can be merged.
1830
19 >>> admin_browser.getControl('Deactivate Members and Merge').click()31 >>> registry_browser.getControl('Deactivate Members and Merge').click()
20 >>> admin_browser.url32 >>> registry_browser.url
21 'http://launchpad.dev/~guadamen'33 'http://launchpad.dev/~guadamen'
2234
23 >>> from canonical.launchpad.ftests import ANONYMOUS, login, logout35 >>> from canonical.launchpad.ftests import ANONYMOUS, login, logout
24 >>> from canonical.launchpad.interfaces import IMailingListSet, IPersonSet36 >>> from canonical.launchpad.interfaces import IMailingListSet
25 >>> from zope.component import getUtility37 >>> from zope.component import getUtility
26 >>> login(ANONYMOUS)38 >>> login(ANONYMOUS)
27 >>> person_set = getUtility(IPersonSet)39 >>> new_team.merged.name
28 >>> landscape = person_set.getByName(
29 ... 'landscape-developers-merged', ignore_merged=False)
30 >>> landscape.merged.name
31 u'guadamen'40 u'guadamen'
32 >>> logout()41 >>> logout()
3342
@@ -36,7 +45,7 @@
3645
37Merged teams are not visible anymore.46Merged teams are not visible anymore.
3847
39 >>> browser.open("http://launchpad.dev/~landscape-developers-merged")48 >>> browser.open("http://launchpad.dev/~new-team-merged")
40 Traceback (most recent call last):49 Traceback (most recent call last):
41 ...50 ...
42 NotFound:...51 NotFound:...
@@ -48,45 +57,45 @@
48merged, though.57merged, though.
4958
50 >>> login(ANONYMOUS)59 >>> login(ANONYMOUS)
51 >>> guadamen = person_set.getByName('guadamen')60 >>> guadamen = getUtility(IPersonSet).getByName('guadamen')
52 >>> mailing_list = getUtility(IMailingListSet).new(guadamen)61 >>> mailing_list = getUtility(IMailingListSet).new(guadamen)
53 >>> logout()62 >>> logout()
5463
55 >>> admin_browser.open('http://launchpad.dev/people/+adminteammerge')64 >>> registry_browser.open('http://launchpad.dev/people/+adminteammerge')
56 >>> admin_browser.getControl('Duplicated Team').value = 'guadamen'65 >>> registry_browser.getControl('Duplicated Team').value = 'guadamen'
57 >>> admin_browser.getControl('Target Team').value = 'ubuntu-team'66 >>> registry_browser.getControl('Target Team').value = 'ubuntu-team'
58 >>> admin_browser.getControl('Merge').click()67 >>> registry_browser.getControl('Merge').click()
5968
60 >>> admin_browser.url69 >>> registry_browser.url
61 'http://launchpad.dev/people/+adminteammerge'70 'http://launchpad.dev/people/+adminteammerge'
6271
63 >>> print get_feedback_messages(admin_browser.contents)72 >>> print get_feedback_messages(registry_browser.contents)
64 [u'There is 1 error.',73 [u'There is 1 error.',
65 u"guadamen is associated with a Launchpad mailing list;74 u"guadamen is associated with a Launchpad mailing list;
66 we can't merge it."]75 we can't merge it."]
6776
68We also can't (for obvious reasons) merge any person/team into itself.77We also can't (for obvious reasons) merge any person/team into itself.
6978
70 >>> admin_browser.getControl('Duplicated Team').value = 'shipit-admins'79 >>> registry_browser.getControl('Duplicated Team').value = 'shipit-admins'
71 >>> admin_browser.getControl('Target Team').value = 'shipit-admins'80 >>> registry_browser.getControl('Target Team').value = 'shipit-admins'
72 >>> admin_browser.getControl('Merge').click()81 >>> registry_browser.getControl('Merge').click()
7382
74 >>> admin_browser.url83 >>> registry_browser.url
75 'http://launchpad.dev/people/+adminteammerge'84 'http://launchpad.dev/people/+adminteammerge'
7685
77 >>> print get_feedback_messages(admin_browser.contents)86 >>> print get_feedback_messages(registry_browser.contents)
78 [u'There is 1 error.', u"You can't merge shipit-admins into itself."]87 [u'There is 1 error.', u"You can't merge shipit-admins into itself."]
7988
80Nor can we merge a person into a team or a team into a person.89Nor can we merge a person into a team or a team into a person.
8190
82 >>> admin_browser.getControl('Duplicated Team').value = 'ubuntu-team'91 >>> registry_browser.getControl('Duplicated Team').value = 'ubuntu-team'
83 >>> admin_browser.getControl('Target Team').value = 'salgado'92 >>> registry_browser.getControl('Target Team').value = 'salgado'
84 >>> admin_browser.getControl('Merge').click()93 >>> registry_browser.getControl('Merge').click()
8594
86 >>> admin_browser.url95 >>> registry_browser.url
87 'http://launchpad.dev/people/+adminteammerge'96 'http://launchpad.dev/people/+adminteammerge'
8897
89 # Yes, the error message is not of much help, but this UI is only for98 # Yes, the error message is not of much help, but this UI is only for
90 # admins and they're supposed to know what they're doing.99 # admins and they're supposed to know what they're doing.
91 >>> print get_feedback_messages(admin_browser.contents)100 >>> print get_feedback_messages(registry_browser.contents)
92 [u'There is 1 error.', u'Constraint not satisfied']101 [u'There is 1 error.', u'Constraint not satisfied']