Merge lp:~henninge/launchpad/bug-503454 into lp:launchpad
- bug-503454
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Henning Eggers |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~henninge/launchpad/bug-503454 |
Merge into: | lp:launchpad |
Diff against target: |
612 lines (+376/-68) 8 files modified
lib/canonical/launchpad/doc/celebrities.txt (+32/-1) lib/canonical/launchpad/interfaces/launchpad.py (+98/-0) lib/canonical/launchpad/security.py (+20/-13) lib/canonical/launchpad/tests/test_personroles.py (+151/-0) lib/canonical/launchpad/utilities/personroles.py (+55/-0) lib/canonical/launchpad/zcml/launchpad.zcml (+10/-0) lib/lp/services/permission_helpers.py (+0/-46) lib/lp/translations/model/translationimportqueue.py (+10/-8) |
To merge this branch: | bzr merge lp:~henninge/launchpad/bug-503454 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Review via email: mp+17009@code.launchpad.net |
Commit message
Added IPersonRoles to augment ILaunchpadCeleb
Description of the change
Henning Eggers (henninge) wrote : | # |
Henning Eggers (henninge) wrote : | # |
I moved the test from test_personroles.py to celbrities.txt to be sure that somebody adding a celebrity is made aware of the need to update IPersonRoles, too.
Abel Deuring (adeuring) wrote : | # |
Hi Henning,
overall, a very nice branch! r=me with the changes from
http://
We discussed on IRC that it would be better not to use the list of
"SQL names" of the IPerson celebrities for the comparison of the
set person celebrities and the corresponding set of in_.* methods
in IPersonRoles.
Other that that, I have a few mostly formal nitpicks, see below.
r=me
Abel
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -225,6 +225,49 @@
> True
>
>
> +== Person celebrities ==
> +
> +A list of all IPerson celebrities is provided.
> +
> + >>> for name in celebs.
> + ... print name
> + admins
> + bazaar-experts
> + bug-importer
> + bug-watch-updater
> + commercial-admins
> + hwdb-team
> + janitor
> + katie
> + launchpad
> + launchpad-
> + launchpad-
> + mailing-
> + ppa-key-guard
> + registry
> + rosetta-admins
> + shipit-admins
> + techboard
> + ubuntu-branches
> + ubuntu-security
> + vcs-imports
> +
> +The number of Person celebrities is identical to the number of "in_"
> +attributes of the IPersonRoles interface. If they differ, IPersonRoles needs
> +to be synced to ILaunchpadCeleb
> +"in_" attribute(s).
> +
> + >>> from canonical.
> + >>> def number_
> + ... num = 0
> + ... for name in IPersonRoles.
> + ... if name.startswith
> + ... num += 1
> + ... return num
> + >>> print number_
> + True
> +
> +
> == Detecting if a person is a celebrity ==
>
> We can ask if a person has celebrity status.
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -57,6 +57,7 @@
> 'IPasswordChang
> 'IPasswordEncry
> 'IPasswordResets',
> + 'IPersonRoles',
> 'IPrivateApplic
> 'IPrivateMalone
> 'IPrivacy',
> @@ -133,11 +134,110 @@
> ubuntu_techboard = Attribute("The Ubuntu technical board.")
> vcs_imports = Attribute("The 'vcs-imports' team.")
>
> + person_names = Attribute("A list of the names of all celebrity persons.")
> +
> def isCelebrityPers
> """Return true if there is an IPerson celebrity with the given name.
> """
>
>
> +class IPersonRoles(
> + """What celebrity teams a person is member of and similar helpers.
> +
> + Convenience methods that remove frequent calls to ILaunchpadCeleb
> + and IPerson.inTeam from permission checkers. May also be used in model
> + or view code.
> +
> + All person celebrities in ILaunchpadCelbr
> + in_ attribute here and vice versa.
>...
Henning Eggers (henninge) wrote : | # |
> overall, a very nice branch! r=me with the changes from
> http://
>
> We discussed on IRC that it would be better not to use the list of
> "SQL names" of the IPerson celebrities for the comparison of the
> set person celebrities and the corresponding set of in_.* methods
> in IPersonRoles.
Thanks, I did that as seen in the paste and some more. The test will now fail with a message that describes what needs to be done to sync the two Interfaces again. I like this solution a lot. ;)
>
> Other that that, I have a few mostly formal nitpicks, see below.
Cool, please see my comments below.
> > + def isDriver(obj):
> > + """Is this person on of the drivers of the object?"""
>
> s/on of/one of/
Done.
>
> > +
> > + def isOneOf(obj, attributes):
> > + """Is this person on of the roles in relation to the object?
>
> s/on of/one of/
>
Done.
> > +
> > + Check if the person is (inTeam) one of the given IPerson attributes
>
> s/(inTeam) one/(inTeam) of one/
Fixed. Thanks for you patience.
> > @@ -195,7 +194,8 @@
> > usedfor = None
> >
> > def checkAuthentica
> > - return is_admin_
> > + user = IPersonRoles(user)
>
> I find this sort of using one variable for somwhat different thingies
> (first for an IPerson, next for an IPersonRoles) a bit confusing. What
> do you think about "user_roles = IPersonRoles(
>
Yes, I know what you mean. I am not going to do that here, though, because in my next branch, the user parameter will already be IPersonRoles, thus eliminating this line completely again.
But I fixed it in translationimpo
> > === added file 'lib/canonical/
> > + def _test_in_team(self, attribute):
> > + roles_attribute = self.prefix+
>
> Please insert spaces before and after the '+'.
Fixed that, thank you.
> > + def test_in_
> > + # Test all celebrity teams are available.
> > + team_attributes = [
> > + 'admin',
> > + 'bazaar_experts',
> > + 'buildd_admin',
> > + 'commercial_admin',
> > + 'hwdb_team',
> > + 'launchpad_
> > + 'launchpad_
> > + 'mailing_
> > + 'registry_experts',
> > + 'rosetta_experts',
> > + 'shipit_admin',
> > + 'ubuntu_branches',
> > + 'ubuntu_security',
> > + 'ubuntu_techboard',
> > + 'vcs_imports',
> > + ]
>
> This list isn't complete ;) I think you can create the list of
> person celebrity attribute names here in the same way as in
> celebrities.txt in http://
>
That is a great idea and it has the code much shorter. Thanks for that! ;)
> > + for attribute in team_attributes:
> > + self._test_
> > +
> > + def _test_in_
> > + roles_attribute = self.prefix+
>
> Please insert spaces before and after the '+'.
Done.
Thank you for your review, my c...
Henning Eggers (henninge) wrote : | # |
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -8,7 +8,8 @@
to these well-known objects through attributes.
>>> from canonical.
- >>> from canonical.
+ >>> from lp.services.
+ >>> from lp.registry.
>>> celebs = getUtility(
>>> from canonical.
@@ -227,45 +228,32 @@
== Person celebrities ==
-A list of all IPerson celebrities is provided.
-
- >>> for name in celebs.
- ... print name
- admins
- bazaar-experts
- bug-importer
- bug-watch-updater
- commercial-admins
- hwdb-team
- janitor
- katie
- launchpad
- launchpad-
- launchpad-
- mailing-
- ppa-key-guard
- registry
- rosetta-admins
- shipit-admins
- techboard
- ubuntu-branches
- ubuntu-security
- vcs-imports
-
-The number of Person celebrities is identical to the number of "in_"
-attributes of the IPersonRoles interface. If they differ, IPersonRoles needs
+Each person celebrity has a corresponding "in_" attribute in IPersonRoles, to
+check if a person has that role. If the attributes differ, IPersonRoles needs
to be synced to ILaunchpadCeleb
"in_" attribute(s).
>>> from canonical.
- >>> def number_
- ... num = 0
+ >>> def get_person_
+ ... for name in ILaunchpadCeleb
+ ... if IPerson.
+ ... yield "in_" + name
+ >>> def get_person_
... for name in IPersonRoles.
... if name.startswith
- ... num += 1
- ... return num
- >>> print number_
- True
+ ... yield name
+
+Treating the lists as sets and determining their difference gives us a clear
+picture of what is missing where.
+
+ >>> person_
+ >>> person_roles_names = set(get_
+ >>> print "Please add to IPersonRoles: " + (
+ ... ", ".join(
+ Please add to IPersonRoles:
+ >>> print "Please remove from IPersonRoles: " + (
+ ... ", ".join(
+ Please remove from IPersonRoles:
== Detecting if a person is a celebrity ==
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -134,8 +134,6 @@
ubuntu_
Preview Diff
1 | === modified file 'lib/canonical/launchpad/doc/celebrities.txt' |
2 | --- lib/canonical/launchpad/doc/celebrities.txt 2009-11-18 07:19:14 +0000 |
3 | +++ lib/canonical/launchpad/doc/celebrities.txt 2010-01-08 23:43:19 +0000 |
4 | @@ -8,7 +8,8 @@ |
5 | to these well-known objects through attributes. |
6 | |
7 | >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities |
8 | - >>> from canonical.launchpad.interfaces import ILanguageSet, IPersonSet |
9 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
10 | + >>> from lp.registry.interfaces.person import IPerson, IPersonSet |
11 | >>> celebs = getUtility(ILaunchpadCelebrities) |
12 | |
13 | >>> from canonical.launchpad.webapp.testing import verifyObject |
14 | @@ -225,6 +226,36 @@ |
15 | True |
16 | |
17 | |
18 | +== Person celebrities == |
19 | + |
20 | +Each person celebrity has a corresponding "in_" attribute in IPersonRoles, to |
21 | +check if a person has that role. If the attributes differ, IPersonRoles needs |
22 | +to be synced to ILaunchpadCelebrities by adding/removing the appropriate |
23 | +"in_" attribute(s). |
24 | + |
25 | + >>> from canonical.launchpad.interfaces.launchpad import IPersonRoles |
26 | + >>> def get_person_celebrity_names(): |
27 | + ... for name in ILaunchpadCelebrities.names(): |
28 | + ... if IPerson.providedBy(getattr(celebs, name)): |
29 | + ... yield "in_" + name |
30 | + >>> def get_person_roles_names(): |
31 | + ... for name in IPersonRoles.names(): |
32 | + ... if name.startswith("in_"): |
33 | + ... yield name |
34 | + |
35 | +Treating the lists as sets and determining their difference gives us a clear |
36 | +picture of what is missing where. |
37 | + |
38 | + >>> person_celebrity_names = set(get_person_celebrity_names()) |
39 | + >>> person_roles_names = set(get_person_roles_names()) |
40 | + >>> print "Please add to IPersonRoles: " + ( |
41 | + ... ", ".join(list(person_celebrity_names - person_roles_names))) |
42 | + Please add to IPersonRoles: |
43 | + >>> print "Please remove from IPersonRoles: " + ( |
44 | + ... ", ".join(list(person_roles_names - person_celebrity_names))) |
45 | + Please remove from IPersonRoles: |
46 | + |
47 | + |
48 | == Detecting if a person is a celebrity == |
49 | |
50 | We can ask if a person has celebrity status. |
51 | |
52 | === modified file 'lib/canonical/launchpad/interfaces/launchpad.py' |
53 | --- lib/canonical/launchpad/interfaces/launchpad.py 2009-12-03 04:50:40 +0000 |
54 | +++ lib/canonical/launchpad/interfaces/launchpad.py 2010-01-08 23:43:19 +0000 |
55 | @@ -57,6 +57,7 @@ |
56 | 'IPasswordChangeApp', |
57 | 'IPasswordEncryptor', |
58 | 'IPasswordResets', |
59 | + 'IPersonRoles', |
60 | 'IPrivateApplication', |
61 | 'IPrivateMaloneApplication', |
62 | 'IPrivacy', |
63 | @@ -138,6 +139,103 @@ |
64 | """ |
65 | |
66 | |
67 | +class IPersonRoles(Interface): |
68 | + """What celebrity teams a person is member of and similar helpers. |
69 | + |
70 | + Convenience methods that remove frequent calls to ILaunchpadCelebrities |
71 | + and IPerson.inTeam from permission checkers. May also be used in model |
72 | + or view code. |
73 | + |
74 | + All person celebrities in ILaunchpadCelbrities must have a matching |
75 | + in_ attribute here and vice versa. |
76 | + """ |
77 | + |
78 | + person = Attribute("The IPerson object that these checks refer to.") |
79 | + |
80 | + in_admin = Bool( |
81 | + title=_("True if this person is a Launchpad admin."), |
82 | + required=True, readonly=True) |
83 | + in_bazaar_experts = Bool( |
84 | + title=_("True if this person is a Bazaar expert."), |
85 | + required=True, readonly=True) |
86 | + in_bug_importer = Bool( |
87 | + title=_("True if this person is a bug importer."), |
88 | + required=True, readonly=True) |
89 | + in_bug_watch_updater = Bool( |
90 | + title=_("True if this person is a bug watch updater."), |
91 | + required=True, readonly=True) |
92 | + in_buildd_admin = Bool( |
93 | + title=_("True if this person is a buildd admin."), |
94 | + required=True, readonly=True) |
95 | + in_commercial_admin = Bool( |
96 | + title=_("True if this person is a commercial admin."), |
97 | + required=True, readonly=True) |
98 | + in_hwdb_team = Bool( |
99 | + title=_("True if this person is on the hwdb team."), |
100 | + required=True, readonly=True) |
101 | + in_janitor = Bool( |
102 | + title=_("True if this person is the janitor."), |
103 | + required=True, readonly=True) |
104 | + in_katie = Bool( |
105 | + title=_("True if this person is Katie."), |
106 | + required=True, readonly=True) |
107 | + in_launchpad_beta_testers = Bool( |
108 | + title=_("True if this person is a Launchpad beta tester."), |
109 | + required=True, readonly=True) |
110 | + in_launchpad_developers = Bool( |
111 | + title=_("True if this person is a Launchpad developer."), |
112 | + required=True, readonly=True) |
113 | + in_mailing_list_experts = Bool( |
114 | + title=_("True if this person is a mailing list expert."), |
115 | + required=True, readonly=True) |
116 | + in_ppa_key_guard = Bool( |
117 | + title=_("True if this person is the ppa key guard."), |
118 | + required=True, readonly=True) |
119 | + in_registry_experts = Bool( |
120 | + title=_("True if this person is a registry expert."), |
121 | + required=True, readonly=True) |
122 | + in_rosetta_experts = Bool( |
123 | + title=_("True if this person is a rosetta expert."), |
124 | + required=True, readonly=True) |
125 | + in_shipit_admin = Bool( |
126 | + title=_("True if this person is a ShipIt admin."), |
127 | + required=True, readonly=True) |
128 | + in_ubuntu_branches = Bool( |
129 | + title=_("True if this person is on the Ubuntu branches team."), |
130 | + required=True, readonly=True) |
131 | + in_ubuntu_security = Bool( |
132 | + title=_("True if this person is on the Ubuntu security team."), |
133 | + required=True, readonly=True) |
134 | + in_ubuntu_techboard = Bool( |
135 | + title=_("True if this person is on the Ubuntu tech board."), |
136 | + required=True, readonly=True) |
137 | + in_vcs_imports = Bool( |
138 | + title=_("True if this person is on the vcs-imports team."), |
139 | + required=True, readonly=True) |
140 | + |
141 | + def inTeam(team): |
142 | + """Is this person a member or the owner of `team`? |
143 | + |
144 | + Passed through to the same method in 'IPersonPublic'. |
145 | + """ |
146 | + |
147 | + def isOwner(obj): |
148 | + """Is this person the owner of the object?""" |
149 | + |
150 | + def isDriver(obj): |
151 | + """Is this person one of the drivers of the object?""" |
152 | + |
153 | + def isOneOf(obj, attributes): |
154 | + """Is this person one of the roles in relation to the object? |
155 | + |
156 | + Check if the person is inTeam of one of the given IPerson attributes |
157 | + of the object. |
158 | + |
159 | + :param obj: The object to check the relation to. |
160 | + :param attributes: A list of attribute names to check with inTeam. |
161 | + """ |
162 | + |
163 | + |
164 | class ICrowd(Interface): |
165 | |
166 | def __contains__(person_or_team_or_anything): |
167 | |
168 | === modified file 'lib/canonical/launchpad/security.py' |
169 | --- lib/canonical/launchpad/security.py 2010-01-07 06:27:46 +0000 |
170 | +++ lib/canonical/launchpad/security.py 2010-01-08 23:43:19 +0000 |
171 | @@ -53,12 +53,11 @@ |
172 | from canonical.launchpad.interfaces.hwdb import ( |
173 | IHWDBApplication, IHWDevice, IHWDeviceClass, IHWDriver, IHWDriverName, |
174 | IHWDriverPackageName, IHWSubmission, IHWSubmissionDevice, IHWVendorID) |
175 | -from lp.services.permission_helpers import ( |
176 | - is_admin, is_admin_or_registry_expert, is_admin_or_rosetta_expert) |
177 | from lp.services.worlddata.interfaces.language import ILanguage, ILanguageSet |
178 | from lp.translations.interfaces.languagepack import ILanguagePack |
179 | from canonical.launchpad.interfaces.launchpad import ( |
180 | - IBazaarApplication, IHasBug, IHasDrivers, ILaunchpadCelebrities) |
181 | + IBazaarApplication, IHasBug, IHasDrivers, ILaunchpadCelebrities, |
182 | + IPersonRoles) |
183 | from lp.registry.interfaces.role import IHasOwner |
184 | from lp.registry.interfaces.location import IPersonLocation |
185 | from lp.registry.interfaces.mailinglist import IMailingListSet |
186 | @@ -195,7 +194,8 @@ |
187 | usedfor = None |
188 | |
189 | def checkAuthenticated(self, user): |
190 | - return is_admin_or_registry_expert(user) |
191 | + user = IPersonRoles(user) |
192 | + return user.in_admin or user.in_registry_experts |
193 | |
194 | |
195 | class ReviewProduct(ReviewByRegistryExpertsOrAdmins): |
196 | @@ -231,9 +231,10 @@ |
197 | if self.obj.active: |
198 | return True |
199 | else: |
200 | - celebrities = getUtility(ILaunchpadCelebrities) |
201 | - return (user.inTeam(celebrities.commercial_admin) |
202 | - or is_admin_or_registry_expert(user)) |
203 | + user = IPersonRoles(user) |
204 | + return (user.in_commercial_admin or |
205 | + user.in_admin or |
206 | + user.in_registry_experts) |
207 | |
208 | |
209 | class EditAccountBySelfOrAdmin(AuthorizationBase): |
210 | @@ -247,7 +248,8 @@ |
211 | EditAccountBySelfOrAdmin, self).checkAccountAuthenticated(account) |
212 | |
213 | def checkAuthenticated(self, user): |
214 | - return is_admin(user) |
215 | + user = IPersonRoles(user) |
216 | + return user.in_admin |
217 | |
218 | |
219 | class ViewAccount(EditAccountBySelfOrAdmin): |
220 | @@ -259,7 +261,8 @@ |
221 | |
222 | def checkAuthenticated(self, user): |
223 | """Extend permission to registry experts.""" |
224 | - return is_admin_or_registry_expert(user) |
225 | + user = IPersonRoles(user) |
226 | + return user.in_admin or user.in_registry_experts |
227 | |
228 | |
229 | class ModerateAccountByRegistryExpert(AuthorizationBase): |
230 | @@ -267,7 +270,8 @@ |
231 | permission = 'launchpad.Moderate' |
232 | |
233 | def checkAuthenticated(self, user): |
234 | - return is_admin_or_registry_expert(user) |
235 | + user = IPersonRoles(user) |
236 | + return user.in_admin or user.in_registry_experts |
237 | |
238 | |
239 | class EditOAuthAccessToken(AuthorizationBase): |
240 | @@ -498,7 +502,8 @@ |
241 | |
242 | def checkAuthenticated(self, user): |
243 | """Allow Launchpad's admins and Rosetta experts edit all fields.""" |
244 | - return is_admin_or_rosetta_expert(user) |
245 | + user = IPersonRoles(user) |
246 | + return user.in_admin or user.in_rosetta_experts |
247 | |
248 | |
249 | class AdminProductTranslations(AuthorizationBase): |
250 | @@ -511,8 +516,10 @@ |
251 | Any Launchpad/Launchpad Translations administrator or owners are |
252 | able to change translation settings for a product. |
253 | """ |
254 | - return (user.inTeam(self.obj.owner) or |
255 | - is_admin_or_rosetta_expert(user)) |
256 | + user = IPersonRoles(user) |
257 | + return (user.isOwner(self.obj) or |
258 | + user.in_rosetta_experts or |
259 | + user.in_admin) |
260 | |
261 | |
262 | class AdminSeriesByVCSImports(AuthorizationBase): |
263 | |
264 | === added file 'lib/canonical/launchpad/tests/test_personroles.py' |
265 | --- lib/canonical/launchpad/tests/test_personroles.py 1970-01-01 00:00:00 +0000 |
266 | +++ lib/canonical/launchpad/tests/test_personroles.py 2010-01-08 23:43:19 +0000 |
267 | @@ -0,0 +1,151 @@ |
268 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
269 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
270 | + |
271 | +__metaclass__ = type |
272 | + |
273 | +import unittest |
274 | + |
275 | +from zope.interface.verify import verifyObject |
276 | +from zope.component import getUtility |
277 | +from zope.security.proxy import removeSecurityProxy |
278 | + |
279 | +from canonical.launchpad.interfaces.launchpad import ( |
280 | + ILaunchpadCelebrities, IPersonRoles) |
281 | +from lp.registry.interfaces.person import IPerson |
282 | + |
283 | +from lp.testing import TestCaseWithFactory |
284 | +from canonical.testing import ZopelessDatabaseLayer |
285 | + |
286 | + |
287 | +class TestPersonRoles(TestCaseWithFactory): |
288 | + """Test IPersonRoles adapter. |
289 | + """ |
290 | + |
291 | + layer = ZopelessDatabaseLayer |
292 | + |
293 | + prefix = 'in_' |
294 | + |
295 | + def setUp(self): |
296 | + super(TestPersonRoles, self).setUp() |
297 | + self.person = self.factory.makePerson() |
298 | + self.celebs = getUtility(ILaunchpadCelebrities) |
299 | + |
300 | + def test_interface(self): |
301 | + roles = IPersonRoles(self.person) |
302 | + verifyObject(IPersonRoles, roles) |
303 | + |
304 | + def test_person(self): |
305 | + # The person is available through the person attribute. |
306 | + roles = IPersonRoles(self.person) |
307 | + self.assertIs(self.person, roles.person) |
308 | + |
309 | + def _get_person_celebrities(self, is_team): |
310 | + for name in ILaunchpadCelebrities.names(): |
311 | + attr = getattr(self.celebs, name) |
312 | + if IPerson.providedBy(attr) and attr.isTeam() == is_team: |
313 | + yield (name, attr) |
314 | + |
315 | + def test_in_teams(self): |
316 | + # Test all celebrity teams are available. |
317 | + for name, team in self._get_person_celebrities(is_team=True): |
318 | + roles_attribute = self.prefix + name |
319 | + roles = IPersonRoles(self.person) |
320 | + self.assertFalse( |
321 | + getattr(roles, roles_attribute), |
322 | + "%s should be False" % roles_attribute) |
323 | + |
324 | + team.addMember(self.person, team.teamowner) |
325 | + roles = IPersonRoles(self.person) |
326 | + self.assertTrue( |
327 | + getattr(roles, roles_attribute), |
328 | + "%s should be True" % roles_attribute) |
329 | + self.person.leave(team) |
330 | + |
331 | + def test_is_person(self): |
332 | + # All celebrity persons are available. |
333 | + for name, celeb in self._get_person_celebrities(is_team=False): |
334 | + roles_attribute = self.prefix + name |
335 | + roles = IPersonRoles(celeb) |
336 | + self.assertTrue( |
337 | + getattr(roles, roles_attribute), |
338 | + "%s should be True" % roles_attribute) |
339 | + |
340 | + def test_in_AttributeError(self): |
341 | + # Do not check for non-existent attributes, even if it has the |
342 | + # right prefix. |
343 | + roles = IPersonRoles(self.person) |
344 | + fake_attr = self.factory.getUniqueString() |
345 | + self.assertRaises(AttributeError, getattr, roles, fake_attr) |
346 | + fake_attr = self.factory.getUniqueString(self.prefix) |
347 | + self.assertRaises(AttributeError, getattr, roles, fake_attr) |
348 | + |
349 | + def test_inTeam(self): |
350 | + # The method person.inTeam is available as the inTeam attribute. |
351 | + roles = IPersonRoles(self.person) |
352 | + self.assertEquals(self.person.inTeam, roles.inTeam) |
353 | + |
354 | + def test_inTeam_works(self): |
355 | + # Make sure it actually works. |
356 | + team = self.factory.makeTeam(self.person) |
357 | + roles = IPersonRoles(self.person) |
358 | + self.assertTrue(roles.inTeam(team)) |
359 | + |
360 | + def test_isOwner(self): |
361 | + # The person can be the owner of something, e.g. a product. |
362 | + product = self.factory.makeProduct(owner=self.person) |
363 | + roles = IPersonRoles(self.person) |
364 | + self.assertTrue(roles.isOwner(product)) |
365 | + |
366 | + def test_isDriver(self): |
367 | + # The person can be the driver of something, e.g. a sprint. |
368 | + sprint = self.factory.makeSprint() |
369 | + sprint.driver = self.person |
370 | + roles = IPersonRoles(self.person) |
371 | + self.assertTrue(roles.isDriver(sprint)) |
372 | + |
373 | + def test_isDriver_multiple_drivers(self): |
374 | + # The person can be one of multiple drivers of if a product and its |
375 | + # series each has a driver. |
376 | + productseries = self.factory.makeProductSeries() |
377 | + productseries.product.driver = self.person |
378 | + productseries.driver = self.factory.makePerson() |
379 | + roles = IPersonRoles(self.person) |
380 | + self.assertTrue(roles.isDriver(productseries)) |
381 | + |
382 | + def test_isOneOf(self): |
383 | + # Objects may have multiple roles that a person can fulfill. |
384 | + # Specifications are such a case. |
385 | + spec = removeSecurityProxy(self.factory.makeSpecification()) |
386 | + spec.owner = self.factory.makePerson() |
387 | + spec.drafter = self.factory.makePerson() |
388 | + spec.assignee = self.factory.makePerson() |
389 | + spec.approver = self.person |
390 | + |
391 | + roles = IPersonRoles(self.person) |
392 | + self.assertTrue(roles.isOneOf( |
393 | + spec, ['owner', 'drafter', 'assignee', 'approver'])) |
394 | + |
395 | + def test_isOneOf_None(self): |
396 | + # Objects may have multiple roles that a person can fulfill. |
397 | + # Specifications are such a case. Some roles may be None. |
398 | + spec = removeSecurityProxy(self.factory.makeSpecification()) |
399 | + spec.owner = self.factory.makePerson() |
400 | + spec.drafter = None |
401 | + spec.assignee = None |
402 | + spec.approver = self.person |
403 | + |
404 | + roles = IPersonRoles(self.person) |
405 | + self.assertTrue(roles.isOneOf( |
406 | + spec, ['owner', 'drafter', 'assignee', 'approver'])) |
407 | + |
408 | + def test_isOneOf_AttributeError(self): |
409 | + # Do not try to check for none-existent attributes. |
410 | + obj = self.factory.makeProduct() |
411 | + fake_attr = self.factory.getUniqueString() |
412 | + roles = IPersonRoles(self.person) |
413 | + self.assertRaises(AttributeError, roles.isOneOf, obj, [fake_attr]) |
414 | + |
415 | + |
416 | +def test_suite(): |
417 | + return unittest.TestLoader().loadTestsFromName(__name__) |
418 | + |
419 | |
420 | === added file 'lib/canonical/launchpad/utilities/personroles.py' |
421 | --- lib/canonical/launchpad/utilities/personroles.py 1970-01-01 00:00:00 +0000 |
422 | +++ lib/canonical/launchpad/utilities/personroles.py 2010-01-08 23:43:19 +0000 |
423 | @@ -0,0 +1,55 @@ |
424 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
425 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
426 | + |
427 | +"""Class that implements the IPersonRoles interface.""" |
428 | + |
429 | +__metaclass__ = type |
430 | +__all__ = ['PersonRoles'] |
431 | + |
432 | +from zope.interface import implements |
433 | +from zope.component import adapts, getUtility |
434 | +from canonical.launchpad.interfaces import ( |
435 | + ILaunchpadCelebrities, IPersonRoles) |
436 | + |
437 | +from lp.registry.interfaces.person import IPerson |
438 | + |
439 | + |
440 | +class PersonRoles: |
441 | + implements(IPersonRoles) |
442 | + adapts(IPerson) |
443 | + |
444 | + def __init__(self, person): |
445 | + self.person = person |
446 | + self._celebrities = getUtility(ILaunchpadCelebrities) |
447 | + self.inTeam = self.person.inTeam |
448 | + |
449 | + def __getattr__(self, name): |
450 | + """Handle all in_* attributes.""" |
451 | + prefix = 'in_' |
452 | + if not name.startswith(prefix): |
453 | + raise AttributeError |
454 | + attribute = name[len(prefix):] |
455 | + return self.person.inTeam(getattr(self._celebrities, attribute)) |
456 | + |
457 | + def isOwner(self, obj): |
458 | + """See IPersonRoles.""" |
459 | + return self.person.inTeam(obj.owner) |
460 | + |
461 | + def isDriver(self, obj): |
462 | + """See IPersonRoles.""" |
463 | + drivers = getattr(obj, 'drivers', None) |
464 | + if drivers is None: |
465 | + return self.person.inTeam(obj.driver) |
466 | + for driver in drivers: |
467 | + if self.person.inTeam(driver): |
468 | + return True |
469 | + return False |
470 | + |
471 | + def isOneOf(self, obj, attributes): |
472 | + """See IPersonRoles.""" |
473 | + for attr in attributes: |
474 | + role = getattr(obj, attr) |
475 | + if self.person.inTeam(role): |
476 | + return True |
477 | + return False |
478 | + |
479 | |
480 | === modified file 'lib/canonical/launchpad/zcml/launchpad.zcml' |
481 | --- lib/canonical/launchpad/zcml/launchpad.zcml 2009-12-23 16:17:12 +0000 |
482 | +++ lib/canonical/launchpad/zcml/launchpad.zcml 2010-01-08 23:43:19 +0000 |
483 | @@ -356,6 +356,16 @@ |
484 | |
485 | </facet> |
486 | |
487 | +<class |
488 | + class="canonical.launchpad.utilities.personroles.PersonRoles"> |
489 | + <allow |
490 | + interface="canonical.launchpad.interfaces.IPersonRoles"/> |
491 | +</class> |
492 | +<adapter |
493 | + for="lp.registry.interfaces.person.IPerson" |
494 | + provides="canonical.launchpad.interfaces.IPersonRoles" |
495 | + factory="canonical.launchpad.utilities.personroles.PersonRoles" /> |
496 | + |
497 | |
498 | <adapter |
499 | provides="canonical.lazr.interfaces.IObjectPrivacy" |
500 | |
501 | === removed file 'lib/lp/services/permission_helpers.py' |
502 | --- lib/lp/services/permission_helpers.py 2009-12-15 21:34:43 +0000 |
503 | +++ lib/lp/services/permission_helpers.py 1970-01-01 00:00:00 +0000 |
504 | @@ -1,46 +0,0 @@ |
505 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
506 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
507 | - |
508 | -"""Helpful functions to enforce permissions.""" |
509 | - |
510 | -__metaclass__ = type |
511 | - |
512 | -__all__ = [ |
513 | - 'is_admin', |
514 | - 'is_admin_or_registry_expert', |
515 | - 'is_admin_or_rosetta_expert', |
516 | - 'is_registry_expert', |
517 | - 'is_rosetta_expert', |
518 | - ] |
519 | - |
520 | -from zope.component import getUtility |
521 | - |
522 | -from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
523 | - |
524 | - |
525 | -def is_admin(user): |
526 | - """Check if the user is a Launchpad admin.""" |
527 | - celebrities = getUtility(ILaunchpadCelebrities) |
528 | - return user.inTeam(celebrities.admin) |
529 | - |
530 | - |
531 | -def is_rosetta_expert(user): |
532 | - """Check if the user is a Rosetta expert.""" |
533 | - celebrities = getUtility(ILaunchpadCelebrities) |
534 | - return user.inTeam(celebrities.rosetta_experts) |
535 | - |
536 | - |
537 | -def is_registry_expert(user): |
538 | - """Check if the user is a registry expert.""" |
539 | - celebrities = getUtility(ILaunchpadCelebrities) |
540 | - return user.inTeam(celebrities.registry_experts) |
541 | - |
542 | - |
543 | -def is_admin_or_rosetta_expert(user): |
544 | - """Check if the user is a Launchpad admin or a Rosetta expert.""" |
545 | - return is_admin(user) or is_rosetta_expert(user) |
546 | - |
547 | - |
548 | -def is_admin_or_registry_expert(user): |
549 | - """Check if the user is a Launchpad admin or a registry expert.""" |
550 | - return is_admin(user) or is_registry_expert(user) |
551 | |
552 | === modified file 'lib/lp/translations/model/translationimportqueue.py' |
553 | --- lib/lp/translations/model/translationimportqueue.py 2009-12-24 11:34:12 +0000 |
554 | +++ lib/lp/translations/model/translationimportqueue.py 2010-01-08 23:43:19 +0000 |
555 | @@ -32,7 +32,8 @@ |
556 | from canonical.database.constants import UTC_NOW, DEFAULT |
557 | from canonical.database.enumcol import EnumCol |
558 | from canonical.launchpad.helpers import shortlist |
559 | -from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
560 | +from canonical.launchpad.interfaces.launchpad import ( |
561 | + ILaunchpadCelebrities, IPersonRoles) |
562 | from canonical.launchpad.interfaces.lpstorm import IMasterStore |
563 | from canonical.launchpad.webapp.interfaces import NotFoundError |
564 | from lp.registry.interfaces.distribution import IDistribution |
565 | @@ -43,7 +44,6 @@ |
566 | from lp.registry.interfaces.product import IProduct |
567 | from lp.registry.interfaces.productseries import IProductSeries |
568 | from lp.registry.interfaces.sourcepackage import ISourcePackage |
569 | -from lp.services.permission_helpers import is_admin_or_rosetta_expert |
570 | from lp.services.worlddata.interfaces.language import ILanguageSet |
571 | from lp.translations.interfaces.pofile import IPOFileSet |
572 | from lp.translations.interfaces.potemplate import IPOTemplateSet |
573 | @@ -285,12 +285,13 @@ |
574 | |
575 | def isUserUploaderOrOwner(self, user): |
576 | """See `ITranslationImportQueueEntry`.""" |
577 | - if user.inTeam(self.importer): |
578 | + roles = IPersonRoles(user) |
579 | + if roles.inTeam(self.importer): |
580 | return True |
581 | if self.productseries is not None: |
582 | - return user.inTeam(self.productseries.product.owner) |
583 | + return roles.isOwner(self.productseries.product) |
584 | if self.distroseries is not None: |
585 | - return user.inTeam(self.distroseries.distribution.owner) |
586 | + return roles.isOwner(self.distroseries.distribution) |
587 | return False |
588 | |
589 | def canSetStatus(self, new_status, user): |
590 | @@ -298,7 +299,8 @@ |
591 | if user is None: |
592 | # Anonymous user cannot do anything. |
593 | return False |
594 | - can_admin = (is_admin_or_rosetta_expert(user) or |
595 | + roles = IPersonRoles(user) |
596 | + can_admin = (roles.in_admin or roles.in_rosetta_experts or |
597 | self.isUbuntuAndIsUserTranslationGroupOwner(user)) |
598 | if new_status == RosettaImportStatus.APPROVED: |
599 | # Only administrators are able to set the APPROVED status, and |
600 | @@ -309,11 +311,11 @@ |
601 | # Only rosetta experts are able to set the IMPORTED status, and |
602 | # that's only possible if we know where to import it |
603 | # (import_into not None). |
604 | - return (is_admin_or_rosetta_expert(user) and |
605 | + return ((roles.in_admin or roles.in_rosetta_experts) and |
606 | self.import_into is not None) |
607 | if new_status == RosettaImportStatus.FAILED: |
608 | # Only rosetta experts are able to set the FAILED status. |
609 | - return is_admin_or_rosetta_expert(user) |
610 | + return roles.in_admin or roles.in_rosetta_experts |
611 | # All other statuses can bset set by all authorized persons. |
612 | return self.isUserUploaderOrOwner(user) or can_admin |
613 |
= Details =
See bug 503454. This is a branch that reliefs me of some techdebt that I incurred when I introduced the file permission_ helpers. py. It was conclude on the mailing list that such specific functions (like is_admin_ or_rosetta_ expert) are not usefull. On the other hand, I still disliked the overhead needed to check if a person is an admin. So I proposed this adapter and discussed it with Curtis, wo agreed that it is a good idea. Curtis and I agreed on the name PersonRoles for the adapter because it resembles goes in the same direction as the roles concept that he had put forward earlier.
This branch introduces PersonRoles and removes permission_ helpers. py, replacing all references to its functions with checks on attributes of PersonRoles. A follow-up branch will make AuthorizationBase use the adapter directly.
Care was taken to make sure that ILaunchpadCeleb rities and IPersonRoles stay in sync, so that whenever a person celebrity is added or removed, the corresponding entry in IPersonRoles has to be added or removed, too.
== Implementation details ==
lib/canonical/ launchpad/ doc/celebrities .txt
* Added a test for celebs. person_ names.
lib/canonical/ launchpad/ interfaces/ launchpad. py
* Added the person_names attribute to ILaunchpadCeleb rities. This is mainly ities in sync
intended to be used by the test that keeps ILauchpatCelebr
with IPersonRoles while tests should not be importing from model code.
* Added IPersonRoles. I placed this in this file intentionally instead of lbrities. The attributes are named exactly like the rities, prefixed with "in_".
giving it its own because it needs to be kept synced with
ILaunchpadCe
corresponding attributes in ILaunchpadCeleb
lib/canonical/ launchpad/ security. py
* Removed import for permission_helpers because that file is deleted.
* Replaced uses of functions from permission_helpers with checks on the
adapter attributes. The adapter still needs to be applied locally in each
function.
lib/canonical/ launchpad/ tests/test_ personroles. py
* Added interface test for PersonRoles. This will fail if a person celbrity
is removed from ILaunchpadCelebrity but not from IPersonRoles, or such
an attribute has been renamed.
* Added test that checks number of person celebrities in both interfaces. rities
This will fail when a person celebtrity is added to ILaunchpadCeleb
but not to IPersonRoles.
* Added coverage for the "in_" attributes for all currently known person
celebrities, most of which are teams, some are individuals.
* The rest of the test case is concerned with the methods as defined in
IPersonRoles.
lib/canonical/ launchpad/ utilities/ celebrities. py
* Added implementation for person_names. PersonCelebrity Descritor was
already collection them in a "set" attribute.
lib/canonical/ launchpad/ utilities/ personroles. py
* Implemented the "in_" attributes using __getattr__, thus automatically rities
getting the corresponding attribute from the ILaunchpadCeleb
implementation by stripping the prefix off the name.
* isDriver is implemented to also check for the "drivers" attribute, so that
all possible drivers are checked.
lib/canonical/ launc.. .