Merge lp:~wallyworld/launchpad/private-owned-entity-traversal-2 into lp:launchpad
- private-owned-entity-traversal-2
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Curtis Hovey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 14490 |
Proposed branch: | lp:~wallyworld/launchpad/private-owned-entity-traversal-2 |
Merge into: | lp:launchpad |
Prerequisite: | lp:~wallyworld/launchpad/private-owned-entity-traversal |
Diff against target: |
991 lines (+310/-170) 22 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+5/-5) lib/canonical/launchpad/security.py (+38/-27) lib/canonical/launchpad/webapp/authorization.py (+1/-1) lib/canonical/launchpad/webapp/interaction.py (+2/-1) lib/lp/app/browser/launchpad.py (+2/-2) lib/lp/app/browser/tales.py (+8/-0) lib/lp/app/tests/test_tales.py (+16/-9) lib/lp/bugs/browser/tests/test_bugsubscription_views.py (+1/-1) lib/lp/code/browser/tests/test_branchlisting.py (+4/-3) lib/lp/registry/browser/tests/test_mailinglists.py (+8/-6) lib/lp/registry/browser/tests/test_person_view.py (+12/-8) lib/lp/registry/configure.zcml (+6/-4) lib/lp/registry/doc/person.txt (+2/-1) lib/lp/registry/doc/private-team-visibility.txt (+61/-0) lib/lp/registry/interfaces/person.py (+79/-77) lib/lp/registry/interfaces/role.py (+2/-1) lib/lp/registry/model/personroles.py (+10/-8) lib/lp/registry/stories/webservice/xx-derivedistroseries.txt (+2/-1) lib/lp/registry/tests/test_person_vocabularies.py (+3/-2) lib/lp/registry/tests/test_team_webservice.py (+4/-5) lib/lp/soyuz/tests/test_archive_subscriptions.py (+33/-3) lib/lp/testing/factory.py (+11/-5) |
To merge this branch: | bzr merge lp:~wallyworld/launchpad/private-owned-entity-traversal-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+85283@code.launchpad.net |
Commit message
[r=sinzui] Users with access to private team's PPAs or branches have limited view permission on the team.
Description of the change
This branch is a re-submission of lp:~wallyworld/launchpad/private-owned-entity-traversal as described in mp
https:/
Some tweaks were made to fix a traversal issue found during qa on qastaging.
== Implementation ==
The issue was that rendering the ppa page required access to IPerson properties:
- title
- is_probationary
These properties were defined under IPersonViewRest
title simple delegates to displayname
is_probationary returns false for teams anyway
So I simply moved the properties to IPersonLimitedView.
== Tests ==
Add one new test to test the failure as seen on qastaging:
TestArchiveSubs
== Lint ==
Linting changed files:
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' |
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-12-12 02:45:10 +0000 |
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-12-12 05:28:08 +0000 |
4 | @@ -135,7 +135,7 @@ |
5 | ) |
6 | from lp.registry.interfaces.person import ( |
7 | IPerson, |
8 | - IPersonPublic, |
9 | + IPersonViewRestricted, |
10 | ITeam, |
11 | ) |
12 | from lp.registry.interfaces.pillar import ( |
13 | @@ -310,10 +310,10 @@ |
14 | |
15 | IPreviewDiff['branch_merge_proposal'].schema = IBranchMergeProposal |
16 | |
17 | -patch_reference_property(IPersonPublic, 'archive', IArchive) |
18 | -patch_collection_property(IPersonPublic, 'ppas', IArchive) |
19 | -patch_entry_return_type(IPersonPublic, 'getPPAByName', IArchive) |
20 | -patch_entry_return_type(IPersonPublic, 'createPPA', IArchive) |
21 | +patch_reference_property(IPersonViewRestricted, 'archive', IArchive) |
22 | +patch_collection_property(IPersonViewRestricted, 'ppas', IArchive) |
23 | +patch_entry_return_type(IPersonViewRestricted, 'getPPAByName', IArchive) |
24 | +patch_entry_return_type(IPersonViewRestricted, 'createPPA', IArchive) |
25 | |
26 | IHasBuildRecords['getBuildRecords'].queryTaggedValue( |
27 | LAZR_WEBSERVICE_EXPORTED)[ |
28 | |
29 | === modified file 'lib/canonical/launchpad/security.py' |
30 | --- lib/canonical/launchpad/security.py 2011-12-12 02:45:10 +0000 |
31 | +++ lib/canonical/launchpad/security.py 2011-12-12 05:28:08 +0000 |
32 | @@ -63,6 +63,7 @@ |
33 | IBranch, |
34 | user_has_special_branch_access, |
35 | ) |
36 | +from lp.code.interfaces.branchcollection import IBranchCollection |
37 | from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal |
38 | from lp.code.interfaces.branchmergequeue import IBranchMergeQueue |
39 | from lp.code.interfaces.codeimport import ICodeImport |
40 | @@ -800,6 +801,35 @@ |
41 | if (invitee.is_team and |
42 | invitee in user.person.getAdministratedTeams()): |
43 | return True |
44 | + return False |
45 | + |
46 | + |
47 | +class PublicOrPrivateTeamsExistence(AuthorizationBase): |
48 | + """Restrict knowing about private teams' existence. |
49 | + |
50 | + Knowing the existence of a private team allow traversing to its URL and |
51 | + displaying basic information like name, displayname. |
52 | + """ |
53 | + permission = 'launchpad.LimitedView' |
54 | + usedfor = IPersonLimitedView |
55 | + |
56 | + def checkUnauthenticated(self): |
57 | + """Unauthenticated users can only view public teams.""" |
58 | + if self.obj.visibility == PersonVisibility.PUBLIC: |
59 | + return True |
60 | + return False |
61 | + |
62 | + def checkAuthenticated(self, user): |
63 | + """By default, we simply perform a View permission check. |
64 | + |
65 | + We also grant limited viewability to users who can see PPAs and |
66 | + branches owned by the team. In other scenarios, the context in which |
67 | + the permission is required is responsible for pre-caching the |
68 | + launchpad.LimitedView permission on each team which requires it. |
69 | + """ |
70 | + if self.forwardCheckAuthenticated( |
71 | + user, self.obj, 'launchpad.View'): |
72 | + return True |
73 | |
74 | if (self.obj.is_team |
75 | and self.obj.visibility == PersonVisibility.PRIVATE): |
76 | @@ -813,33 +843,14 @@ |
77 | ppa.id for ppa in self.obj.ppas if ppa.private) |
78 | if len(subscriber_archive_ids.intersection(team_ppa_ids)) > 0: |
79 | return True |
80 | - return False |
81 | - |
82 | - |
83 | -class PublicOrPrivateTeamsExistence(AuthorizationBase): |
84 | - """Restrict knowing about private teams' existence. |
85 | - |
86 | - Knowing the existence of a private team allow traversing to its URL and |
87 | - displaying basic information like name, displayname. |
88 | - """ |
89 | - permission = 'launchpad.LimitedView' |
90 | - usedfor = IPersonLimitedView |
91 | - |
92 | - def checkUnauthenticated(self): |
93 | - """Unauthenticated users can only view public teams.""" |
94 | - if self.obj.visibility == PersonVisibility.PUBLIC: |
95 | - return True |
96 | - return False |
97 | - |
98 | - def checkAuthenticated(self, user): |
99 | - """By default, we simply perform a View permission check. |
100 | - |
101 | - The context in which the permission is required is |
102 | - responsible for pre-caching the launchpad.LimitedView permission on |
103 | - each team which requires it. |
104 | - """ |
105 | - return self.forwardCheckAuthenticated( |
106 | - user, self.obj, 'launchpad.View') |
107 | + |
108 | + # Grant visibility to people with subscriptions to branches owned |
109 | + # by the private team. |
110 | + owned_branches = getUtility(IBranchCollection).ownedBy(self.obj) |
111 | + if owned_branches.visibleByUser(user.person).count() > 0: |
112 | + return True |
113 | + |
114 | + return False |
115 | |
116 | |
117 | class EditPollByTeamOwnerOrTeamAdminsOrAdmins( |
118 | |
119 | === modified file 'lib/canonical/launchpad/webapp/authorization.py' |
120 | --- lib/canonical/launchpad/webapp/authorization.py 2011-12-12 02:45:10 +0000 |
121 | +++ lib/canonical/launchpad/webapp/authorization.py 2011-12-12 05:28:08 +0000 |
122 | @@ -192,7 +192,7 @@ |
123 | objecttoauthorize, {}) |
124 | if permission in object_cache: |
125 | return object_cache[permission] |
126 | - principal = participation.principal |
127 | + principal = removeAllProxies(participation.principal) |
128 | |
129 | if (principal is not None and |
130 | not isinstance(principal, UnauthenticatedPrincipal)): |
131 | |
132 | === modified file 'lib/canonical/launchpad/webapp/interaction.py' |
133 | --- lib/canonical/launchpad/webapp/interaction.py 2011-12-12 02:45:10 +0000 |
134 | +++ lib/canonical/launchpad/webapp/interaction.py 2011-12-12 05:28:08 +0000 |
135 | @@ -163,7 +163,8 @@ |
136 | return setupInteraction(ANONYMOUS, participation) |
137 | else: |
138 | # Bypass zope's security because IEmailAddress.email is not public. |
139 | - naked_email = removeSecurityProxy(person.preferredemail) |
140 | + naked_person = removeSecurityProxy(person) |
141 | + naked_email = removeSecurityProxy(naked_person.preferredemail) |
142 | return setupInteractionByEmail(naked_email.email, participation) |
143 | |
144 | |
145 | |
146 | === modified file 'lib/lp/app/browser/launchpad.py' |
147 | --- lib/lp/app/browser/launchpad.py 2011-12-12 02:45:10 +0000 |
148 | +++ lib/lp/app/browser/launchpad.py 2011-12-12 05:28:08 +0000 |
149 | @@ -724,8 +724,8 @@ |
150 | # Check to see if this is a team, and if so, whether the |
151 | # logged in user is allowed to view the team, by virtue of |
152 | # team membership or Launchpad administration. |
153 | - if (person.is_team |
154 | - and not check_permission('launchpad.View', person)): |
155 | + if (person.is_team and |
156 | + not check_permission('launchpad.LimitedView', person)): |
157 | raise NotFound(self.context, name) |
158 | # Only admins are permitted to see suspended users. |
159 | if person.account_status == AccountStatus.SUSPENDED: |
160 | |
161 | === modified file 'lib/lp/app/browser/tales.py' |
162 | --- lib/lp/app/browser/tales.py 2011-12-12 02:45:10 +0000 |
163 | +++ lib/lp/app/browser/tales.py 2011-12-12 05:28:08 +0000 |
164 | @@ -1266,6 +1266,14 @@ |
165 | self.hidden) |
166 | return super(TeamFormatterAPI, self).link(view_name, rootsite) |
167 | |
168 | + def icon(self, view_name): |
169 | + team = self._context |
170 | + if not check_permission('launchpad.LimitedView', team): |
171 | + css_class = ObjectImageDisplayAPI(team).sprite_css() |
172 | + return '<span class="' + css_class + '"></span>' |
173 | + else: |
174 | + return super(TeamFormatterAPI, self).icon(view_name) |
175 | + |
176 | def displayname(self, view_name, rootsite=None): |
177 | """See `PersonFormatterAPI`.""" |
178 | person = self._context |
179 | |
180 | === modified file 'lib/lp/app/tests/test_tales.py' |
181 | --- lib/lp/app/tests/test_tales.py 2011-12-12 02:45:10 +0000 |
182 | +++ lib/lp/app/tests/test_tales.py 2011-12-12 05:28:08 +0000 |
183 | @@ -149,18 +149,20 @@ |
184 | self.assertEqual(expected, result) |
185 | |
186 | |
187 | -class TestTalesFormatterAPI(TestCaseWithFactory): |
188 | - """ Test permissions required to access TalesFormatterAPI methods. |
189 | +class TestTeamFormatterAPI(TestCaseWithFactory): |
190 | + """ Test permissions required to access TeamFormatterAPI methods. |
191 | |
192 | A user must have launchpad.LimitedView permission to use |
193 | - TestTalesFormatterAPI with private teams. |
194 | + TeamFormatterAPI with private teams. |
195 | """ |
196 | - layer = DatabaseFunctionalLayer |
197 | + layer = LaunchpadFunctionalLayer |
198 | |
199 | def setUp(self): |
200 | - super(TestTalesFormatterAPI, self).setUp() |
201 | + super(TestTeamFormatterAPI, self).setUp() |
202 | + icon = self.factory.makeLibraryFileAlias( |
203 | + filename='smurf.png', content_type='image/png') |
204 | self.team = self.factory.makeTeam( |
205 | - name='team', displayname='a team', |
206 | + name='team', displayname='a team', icon=icon, |
207 | visibility=PersonVisibility.PRIVATE) |
208 | |
209 | def _make_formatter(self, cache_permission=False): |
210 | @@ -175,10 +177,11 @@ |
211 | request, 'launchpad.LimitedView', [self.team]) |
212 | return formatter, request, any_person |
213 | |
214 | - def _tales_value(self, attr, request): |
215 | + def _tales_value(self, attr, request, path='fmt'): |
216 | # Evaluate the given formatted attribute value on team. |
217 | - return test_tales( |
218 | - "team/fmt:%s" % attr, team=self.team, request=request) |
219 | + result = test_tales( |
220 | + "team/%s:%s" % (path, attr), team=self.team, request=request) |
221 | + return result |
222 | |
223 | def _test_can_view_attribute_no_login(self, attr, hidden=None): |
224 | # Test attribute access with no login. |
225 | @@ -228,6 +231,10 @@ |
226 | def test_can_view_url(self): |
227 | self._test_can_view_attribute('url') |
228 | |
229 | + def test_can_view_icon(self): |
230 | + self._test_can_view_attribute( |
231 | + 'icon', '<span class="sprite team"></span>') |
232 | + |
233 | |
234 | class TestObjectFormatterAPI(TestCaseWithFactory): |
235 | """Tests for ObjectFormatterAPI""" |
236 | |
237 | === modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py' |
238 | --- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-12-12 02:45:10 +0000 |
239 | +++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-12-12 05:28:08 +0000 |
240 | @@ -629,7 +629,7 @@ |
241 | direct_subscriber = self.factory.makeTeam( |
242 | name='team', displayname='Team Name', owner=teamowner, |
243 | visibility=PersonVisibility.PRIVATE) |
244 | - with person_logged_in(direct_subscriber.teamowner): |
245 | + with person_logged_in(teamowner): |
246 | bug.subscribe(direct_subscriber, direct_subscriber.teamowner, |
247 | level=BugNotificationLevel.LIFECYCLE) |
248 | |
249 | |
250 | === modified file 'lib/lp/code/browser/tests/test_branchlisting.py' |
251 | --- lib/lp/code/browser/tests/test_branchlisting.py 2011-12-12 02:45:10 +0000 |
252 | +++ lib/lp/code/browser/tests/test_branchlisting.py 2011-12-12 05:28:08 +0000 |
253 | @@ -613,13 +613,14 @@ |
254 | layer = DatabaseFunctionalLayer |
255 | |
256 | def _make_branch_for_private_team(self): |
257 | + owner = self.factory.makePerson() |
258 | private_team = self.factory.makeTeam( |
259 | - name='shh', displayname='Shh', |
260 | + name='shh', displayname='Shh', owner=owner, |
261 | visibility=PersonVisibility.PRIVATE) |
262 | member = self.factory.makePerson( |
263 | email='member@example.com', password='test') |
264 | - with person_logged_in(private_team.teamowner): |
265 | - private_team.addMember(member, private_team.teamowner) |
266 | + with person_logged_in(owner): |
267 | + private_team.addMember(member, owner) |
268 | branch = self.factory.makeProductBranch(owner=private_team) |
269 | return private_team, member, branch |
270 | |
271 | |
272 | === modified file 'lib/lp/registry/browser/tests/test_mailinglists.py' |
273 | --- lib/lp/registry/browser/tests/test_mailinglists.py 2011-12-12 02:45:10 +0000 |
274 | +++ lib/lp/registry/browser/tests/test_mailinglists.py 2011-12-12 05:28:08 +0000 |
275 | @@ -28,11 +28,12 @@ |
276 | """Verify the content in +mailing-list-portlet.""" |
277 | |
278 | def makeTeamWithMailingList(self, name=None, owner=None, visibility=None): |
279 | + if owner is None: |
280 | + owner = self.factory.makePerson() |
281 | team = self.factory.makeTeam( |
282 | name=name, owner=owner, visibility=visibility) |
283 | - login_person(team.teamowner) |
284 | - team_list = self.factory.makeMailingList( |
285 | - team=team, owner=team.teamowner) |
286 | + login_person(owner) |
287 | + self.factory.makeMailingList(team=team, owner=owner) |
288 | return team |
289 | |
290 | |
291 | @@ -129,10 +130,11 @@ |
292 | |
293 | def test_private_message_message_without_list(self): |
294 | # Private teams have private archives. |
295 | + owner = self.factory.makePerson() |
296 | team = self.factory.makeTeam( |
297 | - visibility=PersonVisibility.PRIVATE) |
298 | - login_person(team.teamowner) |
299 | + owner=owner, visibility=PersonVisibility.PRIVATE) |
300 | + login_person(owner) |
301 | view = create_initialized_view( |
302 | - team, name='+mailinglist', principal=team.teamowner) |
303 | + team, name='+mailinglist', principal=owner) |
304 | element = find_tag_by_id(view(), 'mailing-list-archive') |
305 | self.assertEqual('private', extract_text(element)) |
306 | |
307 | === modified file 'lib/lp/registry/browser/tests/test_person_view.py' |
308 | --- lib/lp/registry/browser/tests/test_person_view.py 2011-12-12 02:45:10 +0000 |
309 | +++ lib/lp/registry/browser/tests/test_person_view.py 2011-12-12 05:28:08 +0000 |
310 | @@ -475,9 +475,11 @@ |
311 | |
312 | def test_active_participations_with_direct_private_team(self): |
313 | # Users cannot see private teams that they are not members of. |
314 | - team = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE) |
315 | - login_person(team.teamowner) |
316 | - team.addMember(self.user, team.teamowner) |
317 | + owner = self.factory.makePerson() |
318 | + team = self.factory.makeTeam( |
319 | + owner=owner, visibility=PersonVisibility.PRIVATE) |
320 | + login_person(owner) |
321 | + team.addMember(self.user, owner) |
322 | # The team is included in active_participations. |
323 | login_person(self.user) |
324 | view = create_view( |
325 | @@ -492,11 +494,13 @@ |
326 | |
327 | def test_active_participations_with_indirect_private_team(self): |
328 | # Users cannot see private teams that they are not members of. |
329 | - team = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE) |
330 | - direct_team = self.factory.makeTeam(owner=team.teamowner) |
331 | - login_person(team.teamowner) |
332 | - direct_team.addMember(self.user, team.teamowner) |
333 | - team.addMember(direct_team, team.teamowner) |
334 | + owner = self.factory.makePerson() |
335 | + team = self.factory.makeTeam( |
336 | + owner=owner, visibility=PersonVisibility.PRIVATE) |
337 | + direct_team = self.factory.makeTeam(owner=owner) |
338 | + login_person(owner) |
339 | + direct_team.addMember(self.user, owner) |
340 | + team.addMember(direct_team, owner) |
341 | # The team is included in active_participations. |
342 | login_person(self.user) |
343 | view = create_view( |
344 | |
345 | === modified file 'lib/lp/registry/configure.zcml' |
346 | --- lib/lp/registry/configure.zcml 2011-12-12 02:45:10 +0000 |
347 | +++ lib/lp/registry/configure.zcml 2011-12-12 05:28:08 +0000 |
348 | @@ -912,8 +912,6 @@ |
349 | <allow |
350 | interface="lp.registry.interfaces.person.IPersonPublic"/> |
351 | <allow |
352 | - interface="lp.registry.interfaces.person.ITeamPublic"/> |
353 | - <allow |
354 | interface="lp.registry.interfaces.person.IHasStanding"/> |
355 | <allow |
356 | interface="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/> |
357 | @@ -924,6 +922,9 @@ |
358 | permission="launchpad.View" |
359 | interface="lp.registry.interfaces.person.IPersonViewRestricted"/> |
360 | <require |
361 | + permission="launchpad.View" |
362 | + interface="lp.registry.interfaces.person.ITeamPublic"/> |
363 | + <require |
364 | permission="launchpad.Edit" |
365 | interface="lp.registry.interfaces.location.ISetLocation"/> |
366 | <require |
367 | @@ -931,9 +932,10 @@ |
368 | interface="lp.registry.interfaces.person.IPersonEditRestricted"/> |
369 | <require |
370 | permission="launchpad.Edit" |
371 | - set_schema="lp.registry.interfaces.person.IPersonPublic |
372 | + set_schema="lp.registry.interfaces.person.IPersonViewRestricted |
373 | + lp.registry.interfaces.person.IPersonPublic |
374 | lp.registry.interfaces.person.ITeamPublic" |
375 | - set_attributes="displayname"/> |
376 | + set_attributes="displayname icon logo"/> |
377 | <require |
378 | permission="launchpad.Moderate" |
379 | set_attributes="name"/> |
380 | |
381 | === modified file 'lib/lp/registry/doc/person.txt' |
382 | --- lib/lp/registry/doc/person.txt 2011-12-12 02:45:10 +0000 |
383 | +++ lib/lp/registry/doc/person.txt 2011-12-12 05:28:08 +0000 |
384 | @@ -696,6 +696,7 @@ |
385 | >>> private_team = factory.makeTeam(salgado, name='private-team', |
386 | ... displayname='Private Team', |
387 | ... visibility=PersonVisibility.PRIVATE) |
388 | + >>> private_team_owner = private_team.teamowner |
389 | >>> bug_subscription = BugSubscription(bug=bug, person=private_team, |
390 | ... subscribed_by=guadamen) |
391 | |
392 | @@ -806,7 +807,7 @@ |
393 | |
394 | But Owner, a member of that team, will see it in the results. |
395 | |
396 | - >>> login_person(private_team.teamowner) |
397 | + >>> login_person(private_team_owner) |
398 | >>> print_people(personset.find('team')) |
399 | Another a new team (new3): [] |
400 | Hoary Gnome Team (name21): [] |
401 | |
402 | === modified file 'lib/lp/registry/doc/private-team-visibility.txt' |
403 | --- lib/lp/registry/doc/private-team-visibility.txt 2011-12-12 02:45:10 +0000 |
404 | +++ lib/lp/registry/doc/private-team-visibility.txt 2011-12-12 05:28:08 +0000 |
405 | @@ -107,3 +107,64 @@ |
406 | ... |
407 | Unauthorized: (<Person at ... priv-team (Priv Team)>, |
408 | 'name', 'launchpad.LimitedView') |
409 | + |
410 | +A person with visibility to any of the branches owned by the private team will |
411 | +be granted limited view permission on the team. |
412 | + |
413 | +For private branches, a user needs to be subscribed to the branch for the |
414 | +branch (and hence team) to be visible. |
415 | + |
416 | + >>> login_person(priv_owner) |
417 | + >>> private_team_branch = factory.makeBranch( |
418 | + ... owner=priv_team, private=True) |
419 | + >>> login_person(pub_member) |
420 | + >>> check_permission('launchpad.LimitedView', priv_team) |
421 | + False |
422 | + |
423 | + >>> login_person(priv_owner) |
424 | + >>> sub = factory.makeBranchSubscription( |
425 | + ... branch=private_team_branch, person=pub_member, |
426 | + ... subscribed_by=priv_owner) |
427 | + |
428 | + >>> login_person(pub_member) |
429 | + >>> check_permission('launchpad.LimitedView', priv_team) |
430 | + True |
431 | + |
432 | +Subscribers to the teams private PPA have limited view permission. |
433 | + |
434 | + >>> login_person(priv_owner) |
435 | + >>> archive = factory.makeArchive(private=True, owner=priv_team) |
436 | + >>> archive_subscriber = factory.makePerson() |
437 | + >>> sub = archive.newSubscription( |
438 | + ... archive_subscriber, registrant=archive.owner) |
439 | + |
440 | + >>> login_person(archive_subscriber) |
441 | + >>> check_permission('launchpad.LimitedView', priv_team) |
442 | + True |
443 | + |
444 | +Users with LimitedView can know identifying information like name, |
445 | +displayname, and unique_name, but cannot know other information like |
446 | +teamowner. |
447 | + |
448 | + >>> print priv_team.name |
449 | + priv-team |
450 | + |
451 | + >>> print priv_team.displayname |
452 | + Priv Team |
453 | + |
454 | + >>> print priv_team.unique_displayname |
455 | + Priv Team (priv-team) |
456 | + |
457 | + >>> print priv_team.icon |
458 | + None |
459 | + |
460 | + >>> print priv_team.allmembers |
461 | + Traceback (most recent call last): |
462 | + ... |
463 | + Unauthorized: (<Person at ... priv-team (Priv Team)>, |
464 | + 'allmembers', 'launchpad.View') |
465 | + |
466 | +Anonymous users do not have permission. |
467 | + >>> login(ANONYMOUS) |
468 | + >>> check_permission('launchpad.LimitedView', priv_team) |
469 | + False |
470 | |
471 | === modified file 'lib/lp/registry/interfaces/person.py' |
472 | --- lib/lp/registry/interfaces/person.py 2011-12-12 02:45:10 +0000 |
473 | +++ lib/lp/registry/interfaces/person.py 2011-12-12 05:28:08 +0000 |
474 | @@ -15,7 +15,7 @@ |
475 | 'IObjectReassignment', |
476 | 'IPerson', |
477 | 'IPersonClaim', |
478 | - 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py |
479 | + 'IPersonPublic', |
480 | 'IPersonSet', |
481 | 'IPersonSettings', |
482 | 'ISoftwareCenterAgentAPI', |
483 | @@ -657,26 +657,55 @@ |
484 | required=False, default=False) |
485 | |
486 | |
487 | -class IPersonPublic(IHasBranches, IHasSpecifications, |
488 | - IHasMergeProposals, IHasLogo, IHasMugshot, IHasIcon, |
489 | - IHasLocation, IHasRequestedReviews, IObjectWithLocation, |
490 | - IPrivacy, IHasBugs, IHasRecipes, IHasTranslationImports, |
491 | - IPersonSettings, IQuestionsPerson): |
492 | - """Public attributes for a Person.""" |
493 | +class IPersonPublic(IPrivacy): |
494 | + """Public attributes for a Person. |
495 | + |
496 | + Very few attributes on a person can be public because private teams |
497 | + are also persons. The public attributes are generally information |
498 | + needed by the system to determine if the principal in the current |
499 | + interaction can work with the object. |
500 | + """ |
501 | |
502 | id = Int(title=_('ID'), required=True, readonly=True) |
503 | - account = Object(schema=IAccount) |
504 | - accountID = Int(title=_('Account ID'), required=True, readonly=True) |
505 | - password = PasswordField( |
506 | - title=_('Password'), required=True, readonly=False) |
507 | - karma = exported( |
508 | - Int(title=_('Karma'), readonly=True, |
509 | - description=_('The cached total karma for this person.'))) |
510 | - homepage_content = exported( |
511 | - Text(title=_("Homepage Content"), required=False, |
512 | - description=_( |
513 | - "The content of your profile page. Use plain text, " |
514 | - "paragraphs are preserved and URLs are linked in pages."))) |
515 | + # This is redefined from IPrivacy.private because the attribute is |
516 | + # read-only. It is a summary of the team's visibility. |
517 | + private = exported(Bool( |
518 | + title=_("This team is private"), |
519 | + readonly=True, required=False, |
520 | + description=_("Private teams are visible only to " |
521 | + "their members."))) |
522 | + is_valid_person = Bool( |
523 | + title=_("This is an active user and not a team."), readonly=True) |
524 | + is_valid_person_or_team = exported( |
525 | + Bool(title=_("This is an active user or a team."), readonly=True), |
526 | + exported_as='is_valid') |
527 | + is_merge_pending = exported(Bool( |
528 | + title=_("Is this person due to be merged with another?"), |
529 | + required=False, default=False)) |
530 | + is_team = exported( |
531 | + Bool(title=_('Is this object a team?'), readonly=True)) |
532 | + |
533 | + |
534 | +class IPersonLimitedView(IHasIcon, IHasLogo): |
535 | + """IPerson attributes that require launchpad.LimitedView permission.""" |
536 | + |
537 | + name = exported( |
538 | + PersonNameField( |
539 | + title=_('Name'), required=True, readonly=False, |
540 | + constraint=name_validator, |
541 | + description=_( |
542 | + "A short unique name, beginning with a lower-case " |
543 | + "letter or number, and containing only letters, " |
544 | + "numbers, dots, hyphens, or plus signs."))) |
545 | + displayname = exported( |
546 | + StrippedTextLine( |
547 | + title=_('Display Name'), required=True, readonly=False, |
548 | + description=_( |
549 | + "Your name as you would like it displayed throughout " |
550 | + "Launchpad. Most people use their full name here.")), |
551 | + exported_as='display_name') |
552 | + unique_displayname = TextLine( |
553 | + title=_('Return a string of the form $displayname ($name).')) |
554 | # NB at this stage we do not allow individual people to have their own |
555 | # icon, only teams get that. People can however have a logo and mugshot |
556 | # The icon is only used for teams; that's why we use /@@/team as the |
557 | @@ -690,7 +719,6 @@ |
558 | "displayed whenever the team name is listed - for example " |
559 | "in listings of bugs or on a person's membership table.")) |
560 | iconID = Int(title=_('Icon ID'), required=True, readonly=True) |
561 | - |
562 | logo = exported( |
563 | LogoImageUpload( |
564 | title=_("Logo"), required=False, |
565 | @@ -701,6 +729,30 @@ |
566 | "is a logo, a small picture or a personal mascot. It should " |
567 | "be no bigger than 50kb in size."))) |
568 | logoID = Int(title=_('Logo ID'), required=True, readonly=True) |
569 | + # title is required for the Launchpad Page Layout main template |
570 | + title = Attribute('Person Page Title') |
571 | + is_probationary = exported( |
572 | + Bool(title=_("Is this a probationary user?"), readonly=True)) |
573 | + |
574 | + |
575 | +class IPersonViewRestricted(IHasBranches, IHasSpecifications, |
576 | + IHasMergeProposals, IHasMugshot, |
577 | + IHasLocation, IHasRequestedReviews, IObjectWithLocation, |
578 | + IHasBugs, IHasRecipes, IHasTranslationImports, |
579 | + IPersonSettings, IQuestionsPerson): |
580 | + """IPerson attributes that require launchpad.View permission.""" |
581 | + account = Object(schema=IAccount) |
582 | + accountID = Int(title=_('Account ID'), required=True, readonly=True) |
583 | + password = PasswordField( |
584 | + title=_('Password'), required=True, readonly=False) |
585 | + karma = exported( |
586 | + Int(title=_('Karma'), readonly=True, |
587 | + description=_('The cached total karma for this person.'))) |
588 | + homepage_content = exported( |
589 | + Text(title=_("Homepage Content"), required=False, |
590 | + description=_( |
591 | + "The content of your profile page. Use plain text, " |
592 | + "paragraphs are preserved and URLs are linked in pages."))) |
593 | |
594 | mugshot = exported(MugshotImageUpload( |
595 | title=_("Mugshot"), required=False, |
596 | @@ -766,15 +818,6 @@ |
597 | # Properties of the Person object. |
598 | karma_category_caches = Attribute( |
599 | 'The caches of karma scores, by karma category.') |
600 | - is_team = exported( |
601 | - Bool(title=_('Is this object a team?'), readonly=True)) |
602 | - is_valid_person = Bool( |
603 | - title=_("This is an active user and not a team."), readonly=True) |
604 | - is_valid_person_or_team = exported( |
605 | - Bool(title=_("This is an active user or a team."), readonly=True), |
606 | - exported_as='is_valid') |
607 | - is_probationary = exported( |
608 | - Bool(title=_("Is this a probationary user?"), readonly=True)) |
609 | is_ubuntu_coc_signer = exported( |
610 | Bool(title=_("Signed Ubuntu Code of Conduct"), |
611 | readonly=True)) |
612 | @@ -879,7 +922,6 @@ |
613 | exported_as='team_owner') |
614 | teamownerID = Int(title=_("The Team Owner's ID or None"), required=False, |
615 | readonly=True) |
616 | - |
617 | preferredemail = exported( |
618 | Reference(title=_("Preferred email address"), |
619 | description=_("The preferred email address for this person. " |
620 | @@ -916,9 +958,6 @@ |
621 | "this is set to None, then this Person has not been merged " |
622 | "into another and is still valid")) |
623 | |
624 | - # title is required for the Launchpad Page Layout main template |
625 | - title = Attribute('Person Page Title') |
626 | - |
627 | archive = exported( |
628 | Reference( |
629 | title=_("Default PPA"), |
630 | @@ -987,18 +1026,6 @@ |
631 | readonly=True, required=False, |
632 | value_type=Reference(schema=Interface))) # HWSubmission |
633 | |
634 | - # This is redefined from IPrivacy.private because the attribute is |
635 | - # read-only. It is a summary of the team's visibility. |
636 | - private = exported(Bool( |
637 | - title=_("This team is private"), |
638 | - readonly=True, required=False, |
639 | - description=_("Private teams are visible only to " |
640 | - "their members."))) |
641 | - |
642 | - is_merge_pending = exported(Bool( |
643 | - title=_("Is this person due to be merged with another?"), |
644 | - required=False, default=False)) |
645 | - |
646 | administrated_teams = Attribute( |
647 | u"the teams that this person/team is an administrator of.") |
648 | |
649 | @@ -1463,32 +1490,6 @@ |
650 | :return: a boolean. |
651 | """ |
652 | |
653 | - |
654 | -class IPersonLimitedView(Interface): |
655 | - """IPerson attributes that require launchpad.LimitedView permission.""" |
656 | - |
657 | - name = exported( |
658 | - PersonNameField( |
659 | - title=_('Name'), required=True, readonly=False, |
660 | - constraint=name_validator, |
661 | - description=_( |
662 | - "A short unique name, beginning with a lower-case " |
663 | - "letter or number, and containing only letters, " |
664 | - "numbers, dots, hyphens, or plus signs."))) |
665 | - displayname = exported( |
666 | - StrippedTextLine( |
667 | - title=_('Display Name'), required=True, readonly=False, |
668 | - description=_( |
669 | - "Your name as you would like it displayed throughout " |
670 | - "Launchpad. Most people use their full name here.")), |
671 | - exported_as='display_name') |
672 | - unique_displayname = TextLine( |
673 | - title=_('Return a string of the form $displayname ($name).')) |
674 | - |
675 | - |
676 | -class IPersonViewRestricted(Interface): |
677 | - """IPerson attributes that require launchpad.View permission.""" |
678 | - |
679 | active_member_count = Attribute( |
680 | "The number of real people who are members of this team.") |
681 | # activemembers.value_type.schema will be set to IPerson once |
682 | @@ -2565,18 +2566,19 @@ |
683 | ]: |
684 | IPersonViewRestricted[name].value_type.schema = IPerson |
685 | |
686 | -IPersonPublic['sub_teams'].value_type.schema = ITeam |
687 | -IPersonPublic['super_teams'].value_type.schema = ITeam |
688 | +IPersonViewRestricted['sub_teams'].value_type.schema = ITeam |
689 | +IPersonViewRestricted['super_teams'].value_type.schema = ITeam |
690 | # XXX: salgado, 2008-08-01: Uncomment these when teams_*participated_in are |
691 | # exported again. |
692 | -# IPersonPublic['teams_participated_in'].value_type.schema = ITeam |
693 | -# IPersonPublic['teams_indirectly_participated_in'].value_type.schema = ITeam |
694 | +# IPersonViewRestricted['teams_participated_in'].value_type.schema = ITeam |
695 | +# IPersonViewRestricted[ |
696 | +# 'teams_indirectly_participated_in'].value_type.schema = ITeam |
697 | |
698 | # Fix schema of operation parameters. We need zope.deferredimport! |
699 | params_to_fix = [ |
700 | # XXX: salgado, 2008-08-01: Uncomment these when they are exported again. |
701 | - # (IPersonPublic['findPathToTeam'], 'team'), |
702 | - # (IPersonPublic['inTeam'], 'team'), |
703 | + # (IPersonViewRestricted['findPathToTeam'], 'team'), |
704 | + # (IPersonViewRestricted['inTeam'], 'team'), |
705 | (IPersonEditRestricted['join'], 'team'), |
706 | (IPersonEditRestricted['leave'], 'team'), |
707 | (IPersonEditRestricted['addMember'], 'person'), |
708 | |
709 | === modified file 'lib/lp/registry/interfaces/role.py' |
710 | --- lib/lp/registry/interfaces/role.py 2011-12-12 02:45:10 +0000 |
711 | +++ lib/lp/registry/interfaces/role.py 2011-12-12 05:28:08 +0000 |
712 | @@ -119,7 +119,8 @@ |
713 | def inTeam(team): |
714 | """Is this person a member or the owner of `team`? |
715 | |
716 | - Passed through to the same method in 'IPersonPublic'. |
717 | + Passed through to the *unproxied* same method in |
718 | + `IPersonViewRestricted`. |
719 | """ |
720 | |
721 | def isOwner(obj): |
722 | |
723 | === modified file 'lib/lp/registry/model/personroles.py' |
724 | --- lib/lp/registry/model/personroles.py 2011-12-12 02:45:10 +0000 |
725 | +++ lib/lp/registry/model/personroles.py 2011-12-12 05:28:08 +0000 |
726 | @@ -11,6 +11,7 @@ |
727 | getUtility, |
728 | ) |
729 | from zope.interface import implements |
730 | +from zope.security.proxy import removeSecurityProxy |
731 | |
732 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
733 | from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor |
734 | @@ -29,7 +30,8 @@ |
735 | def __init__(self, person): |
736 | self.person = person |
737 | self._celebrities = getUtility(ILaunchpadCelebrities) |
738 | - self.inTeam = self.person.inTeam |
739 | + # Use an unproxied inTeam() method for security checks. |
740 | + self.inTeam = removeSecurityProxy(self.person).inTeam |
741 | |
742 | def __getattr__(self, name): |
743 | """Handle all in_* attributes.""" |
744 | @@ -39,7 +41,7 @@ |
745 | raise AttributeError(errortext) |
746 | attribute = name[len(prefix):] |
747 | try: |
748 | - return self.person.inTeam(getattr(self._celebrities, attribute)) |
749 | + return self.inTeam(getattr(self._celebrities, attribute)) |
750 | except AttributeError: |
751 | raise AttributeError(errortext) |
752 | |
753 | @@ -49,28 +51,28 @@ |
754 | |
755 | def isOwner(self, obj): |
756 | """See IPersonRoles.""" |
757 | - return self.person.inTeam(obj.owner) |
758 | + return self.inTeam(obj.owner) |
759 | |
760 | def isBugSupervisor(self, obj): |
761 | """See IPersonRoles.""" |
762 | return (IHasBugSupervisor.providedBy(obj) |
763 | - and self.person.inTeam(obj.bug_supervisor)) |
764 | + and self.inTeam(obj.bug_supervisor)) |
765 | |
766 | def isSecurityContact(self, obj): |
767 | """See IPersonRoles.""" |
768 | return (IHasSecurityContact.providedBy(obj) |
769 | - and self.person.inTeam(obj.security_contact)) |
770 | + and self.inTeam(obj.security_contact)) |
771 | |
772 | def isDriver(self, obj): |
773 | """See IPersonRoles.""" |
774 | - return self.person.inTeam(obj.driver) |
775 | + return self.inTeam(obj.driver) |
776 | |
777 | def isOneOfDrivers(self, obj): |
778 | """See IPersonRoles.""" |
779 | if not IHasDrivers.providedBy(obj): |
780 | return self.isDriver(obj) |
781 | for driver in obj.drivers: |
782 | - if self.person.inTeam(driver): |
783 | + if self.inTeam(driver): |
784 | return True |
785 | return False |
786 | |
787 | @@ -78,6 +80,6 @@ |
788 | """See IPersonRoles.""" |
789 | for attr in attributes: |
790 | role = getattr(obj, attr) |
791 | - if self.person.inTeam(role): |
792 | + if self.inTeam(role): |
793 | return True |
794 | return False |
795 | |
796 | === modified file 'lib/lp/registry/stories/webservice/xx-derivedistroseries.txt' |
797 | --- lib/lp/registry/stories/webservice/xx-derivedistroseries.txt 2011-12-12 02:45:10 +0000 |
798 | +++ lib/lp/registry/stories/webservice/xx-derivedistroseries.txt 2011-12-12 05:28:08 +0000 |
799 | @@ -19,6 +19,7 @@ |
800 | >>> soyuz_team = factory.makeTeam( |
801 | ... name='soyuz-team', |
802 | ... subscription_policy=TeamSubscriptionPolicy.RESTRICTED) |
803 | + >>> soyuz_team_owner = soyuz_team.teamowner |
804 | >>> parent_series = factory.makeDistroSeries(name="parentseries") |
805 | >>> arch = factory.makeDistroArchSeries(distroseries=parent_series) |
806 | >>> removeSecurityProxy(parent_series).nominatedarchindep = arch |
807 | @@ -41,7 +42,7 @@ |
808 | >>> logout() |
809 | |
810 | >>> soyuz_team_webservice = webservice_for_person( |
811 | - ... soyuz_team.teamowner, permission=OAuthPermission.WRITE_PUBLIC) |
812 | + ... soyuz_team_owner, permission=OAuthPermission.WRITE_PUBLIC) |
813 | |
814 | |
815 | Calling |
816 | |
817 | === modified file 'lib/lp/registry/tests/test_person_vocabularies.py' |
818 | --- lib/lp/registry/tests/test_person_vocabularies.py 2011-12-12 02:45:10 +0000 |
819 | +++ lib/lp/registry/tests/test_person_vocabularies.py 2011-12-12 05:28:08 +0000 |
820 | @@ -311,9 +311,10 @@ |
821 | def test_private_team_cannot_be_a_member_of_itself(self): |
822 | # A private team should be filtered by the vocab.extra_clause |
823 | # when provided a search term. |
824 | + owner = self.factory.makePerson() |
825 | team = self.factory.makeTeam( |
826 | - visibility=PersonVisibility.PRIVATE) |
827 | - login_person(team.teamowner) |
828 | + owner=owner, visibility=PersonVisibility.PRIVATE) |
829 | + login_person(owner) |
830 | self.assertNotIn(team, self.searchVocabulary(team, team.name)) |
831 | |
832 | |
833 | |
834 | === modified file 'lib/lp/registry/tests/test_team_webservice.py' |
835 | --- lib/lp/registry/tests/test_team_webservice.py 2011-12-12 02:45:10 +0000 |
836 | +++ lib/lp/registry/tests/test_team_webservice.py 2011-12-12 05:28:08 +0000 |
837 | @@ -5,8 +5,6 @@ |
838 | |
839 | import httplib |
840 | |
841 | -from zope.security.proxy import removeSecurityProxy |
842 | - |
843 | from lazr.restfulclient.errors import HTTPError |
844 | |
845 | from canonical.testing.layers import DatabaseFunctionalLayer |
846 | @@ -79,8 +77,9 @@ |
847 | # Calling person.join with a team that has an open membership |
848 | # subscription policy should add that that user to the team. |
849 | self.person = self.factory.makePerson(name='test-person') |
850 | - self.team = self.factory.makeTeam(name='test-team') |
851 | - login_person(self.team.teamowner) |
852 | + owner = self.factory.makePerson() |
853 | + self.team = self.factory.makeTeam(name='test-team', owner=owner) |
854 | + login_person(owner) |
855 | self.team.subscriptionpolicy = TeamSubscriptionPolicy.OPEN |
856 | logout() |
857 | |
858 | @@ -88,7 +87,7 @@ |
859 | test_person = launchpad.people['test-person'] |
860 | test_team = launchpad.people['test-team'] |
861 | test_person.join(team=test_team.self_link) |
862 | - login_person(self.team.teamowner) |
863 | + login_person(owner) |
864 | self.assertEqual( |
865 | ['test-team'], |
866 | [membership.team.name |
867 | |
868 | === modified file 'lib/lp/soyuz/tests/test_archive_subscriptions.py' |
869 | --- lib/lp/soyuz/tests/test_archive_subscriptions.py 2011-12-12 02:45:10 +0000 |
870 | +++ lib/lp/soyuz/tests/test_archive_subscriptions.py 2011-12-12 05:28:08 +0000 |
871 | @@ -13,6 +13,7 @@ |
872 | TestCaseWithFactory, |
873 | ) |
874 | from lp.testing.mail_helpers import pop_notifications |
875 | +from lp.testing.views import create_initialized_view |
876 | |
877 | |
878 | class TestArchiveSubscriptions(TestCaseWithFactory): |
879 | @@ -26,9 +27,11 @@ |
880 | def setUp(self): |
881 | """Create a test archive.""" |
882 | super(TestArchiveSubscriptions, self).setUp() |
883 | + self.owner = self.factory.makePerson() |
884 | self.private_team = self.factory.makeTeam( |
885 | - visibility=PersonVisibility.PRIVATE, name="subscribertest") |
886 | - login_person(self.private_team.teamowner) |
887 | + visibility=PersonVisibility.PRIVATE, |
888 | + name="subscribertest", owner=self.owner) |
889 | + login_person(self.owner) |
890 | self.archive = self.factory.makeArchive( |
891 | private=True, owner=self.private_team) |
892 | self.subscriber = self.factory.makePerson() |
893 | @@ -45,7 +48,7 @@ |
894 | login_person(self.subscriber) |
895 | self.assertRaises(Unauthorized, get_name) |
896 | |
897 | - login_person(self.private_team.teamowner) |
898 | + login_person(self.owner) |
899 | self.archive.newSubscription( |
900 | self.subscriber, registrant=self.archive.owner) |
901 | |
902 | @@ -53,6 +56,33 @@ |
903 | login_person(self.subscriber) |
904 | self.assertEqual(self.archive.owner.name, "subscribertest") |
905 | |
906 | + def test_subscriber_can_browse_private_team_ppa(self): |
907 | + # As per bug 597783, we need to make sure a subscriber can see |
908 | + # a private team's PPA after they have been given a subscription. |
909 | + # This test ensures the subscriber can correctly load the PPA's view, |
910 | + # thus ensuring that all attributes necessary to render the view have |
911 | + # the necessary security permissions. |
912 | + |
913 | + # Before a subscription, accessing the view name will raise. |
914 | + login_person(self.subscriber) |
915 | + view = create_initialized_view( |
916 | + self.archive, '+index', principal=self.subscriber) |
917 | + self.assertRaises(Unauthorized, view.render) |
918 | + |
919 | + login_person(self.owner) |
920 | + self.archive.newSubscription( |
921 | + self.subscriber, registrant=self.archive.owner) |
922 | + |
923 | + # When a subscription exists, it's fine. |
924 | + login_person(self.subscriber) |
925 | + self.assertIn(self.archive.displayname, view.render()) |
926 | + |
927 | + # Just to double check, by default, the subscriber still can't see the |
928 | + # +packages view which requires extra permissions. |
929 | + self.assertRaises( |
930 | + Unauthorized, create_initialized_view, |
931 | + self.archive, '+packages', principal=self.subscriber) |
932 | + |
933 | def test_new_subscription_sends_email(self): |
934 | # Creating a new subscription sends an email to all members |
935 | # of the person or team subscribed. |
936 | |
937 | === modified file 'lib/lp/testing/factory.py' |
938 | --- lib/lp/testing/factory.py 2011-12-12 02:45:10 +0000 |
939 | +++ lib/lp/testing/factory.py 2011-12-12 05:28:08 +0000 |
940 | @@ -772,7 +772,7 @@ |
941 | address, person, email_status, account) |
942 | |
943 | def makeTeam(self, owner=None, displayname=None, email=None, name=None, |
944 | - description=None, |
945 | + description=None, icon=None, logo=None, |
946 | subscription_policy=TeamSubscriptionPolicy.OPEN, |
947 | visibility=None, members=None): |
948 | """Create and return a new, arbitrary Team. |
949 | @@ -783,9 +783,11 @@ |
950 | :param displayname: The team's display name. If not given we'll use |
951 | the auto-generated name. |
952 | :param description: Team team's description. |
953 | - :type string: |
954 | + :type description string: |
955 | :param email: The email address to use as the team's contact address. |
956 | :type email: string |
957 | + :param icon: The team's icon. |
958 | + :param logo: The team's logo. |
959 | :param subscription_policy: The subscription policy of the team. |
960 | :type subscription_policy: `TeamSubscriptionPolicy` |
961 | :param visibility: The team's visibility. If it's None, the default |
962 | @@ -810,15 +812,19 @@ |
963 | team = getUtility(IPersonSet).newTeam( |
964 | owner, name, displayname, teamdescription=description, |
965 | subscriptionpolicy=subscription_policy) |
966 | + naked_team = removeSecurityProxy(team) |
967 | if visibility is not None: |
968 | # Visibility is normally restricted to launchpad.Commercial, so |
969 | # removing the security proxy as we don't care here. |
970 | - removeSecurityProxy(team).visibility = visibility |
971 | + naked_team.visibility = visibility |
972 | if email is not None: |
973 | team.setContactAddress( |
974 | getUtility(IEmailAddressSet).new(email, team)) |
975 | + if icon is not None: |
976 | + naked_team.icon = icon |
977 | + if logo is not None: |
978 | + naked_team.logo = logo |
979 | if members is not None: |
980 | - naked_team = removeSecurityProxy(team) |
981 | for member in members: |
982 | naked_team.addMember(member, owner) |
983 | return team |
984 | @@ -1124,7 +1130,7 @@ |
985 | |
986 | if registrant is None: |
987 | if owner.is_team: |
988 | - registrant = owner.teamowner |
989 | + registrant = removeSecurityProxy(owner).teamowner |
990 | else: |
991 | registrant = owner |
992 |
Thank you for the prompt follow up. I am going to use ec2 land now so that this might be merged and in buildbot when you awake.