Merge lp:~wgrant/launchpad/structural-subscription-security into lp:launchpad
- structural-subscription-security
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Jonathan Lange |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~wgrant/launchpad/structural-subscription-security |
Merge into: | lp:launchpad |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~wgrant/launchpad/structural-subscription-security |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | last revision only | Approve | |
Gary Poster (community) | Approve | ||
Review via email: mp+10776@code.launchpad.net |
Commit message
[r=gary][ui=none] (wgrant) Move structural subscription security to the model, rather than the view.
Description of the change
William Grant (wgrant) wrote : | # |
Gary Poster (gary) wrote : | # |
This looks very good. Thank you.
I'm sorely tempted to suggest microoptimizations in _userCanAlterSu
Do I understand correctly that the next branch will expose this in the REST API? That's a curiosity question.
And are you going to request a UI review for the error behavior? That is, if I read the code correctly, as of your current branch, if I try to unsubscribe someone for whom I do not have sufficient privileges through the browser, I will get an OOPS page. That's mostly OK if I never get the option in the UI, but not so good otherwise. Is that also going to be addressed, or already addressed in a way that I don't see? That's a more serious question.
If the answer to my UI question is that my concerns are addressed, are unfounded, or will be addressed before this lands, I approve. Otherwise, let's talk.
I'll be optimistic and mark this approved so I'm not blocking you.
William Grant (wgrant) wrote : | # |
> This looks very good. Thank you.
>
> I'm sorely tempted to suggest microoptimizations in _userCanAlterSu
> --in particular, optimizing for what feels like the common case by moving the
> "subscriber is subscribed_by" check as the very first one (that is, before the
> distribution source package stuff). If you object on principal (premature
> microoptimization), I'm fine with it. OTOH, if you like it, go for it.
I think it's more readable as it is, and only a tiny tiny bit less efficient.
> Do I understand correctly that the next branch will expose this in the REST
> API? That's a curiosity question.
That's correct.
> And are you going to request a UI review for the error behavior? That is, if
> I read the code correctly, as of your current branch, if I try to unsubscribe
> someone for whom I do not have sufficient privileges through the browser, I
> will get an OOPS page. That's mostly OK if I never get the option in the UI,
> but not so good otherwise. Is that also going to be addressed, or already
> addressed in a way that I don't see? That's a more serious question.
The view code implements this same restriction. The form that removes
structural subscriptions gives you checkboxes for each team that you administer.
The code behind it also removes from the submitted list of teams any that you
lack privileges for, before calling removeBugSubscr
So, your evil browser unsubscription request will simply be ignored as before.
> If the answer to my UI question is that my concerns are addressed, are
> unfounded, or will be addressed before this lands, I approve. Otherwise,
> let's talk.
>
> I'll be optimistic and mark this approved so I'm not blocking you.
Thanks for the review. I might try to convince somebody else to merge it,
since you're gone.
Gary Poster (gary) wrote : | # |
Thanks for the reply. Sounds good.
> I might try to convince somebody else to merge it,
> since you're gone.
I checked with wgrant, and mwhudson is helping with the merge. Thanks to both.
William Grant (wgrant) wrote : | # |
> Thanks for the reply. Sounds good.
>
> > I might try to convince somebody else to merge it,
> > since you're gone.
>
> I checked with wgrant, and mwhudson is helping with the merge. Thanks to
> both.
So far mwhudson, thumper and jml have tried to ec2test it.
Each time, no email has arrived and the instance has vanished. I am scared.
jml is apparently going to run the tests on EC2 without going headless,
so he can see what is going wrong...
William Grant (wgrant) wrote : | # |
ec2test eventually became happy with my branch, but there was a test failure. I've fixed it in r9216.
Julian Edwards (julian-edwards) wrote : | # |
<bigjools> wgrant: addSubscription needs an admin?
<wgrant> bigjools: Needs a member of the team.
<wgrant> bigjools: Or an admin.
<wgrant> bigjools: There were no readily accessible team members, so I used an admin.
<bigjools> wgrant: is this fact pertinent to the test, or is it just setting up test conditions?
<wgrant> bigjools: The latter.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/browser/structuralsubscription.py' |
2 | --- lib/canonical/launchpad/browser/structuralsubscription.py 2009-08-24 01:09:07 +0000 |
3 | +++ lib/canonical/launchpad/browser/structuralsubscription.py 2009-08-25 11:08:42 +0000 |
4 | @@ -167,7 +167,7 @@ |
5 | 'e-mail each time someone reports or changes one of ' |
6 | 'its public bugs.' % target.displayname) |
7 | elif is_subscribed and not subscribe: |
8 | - target.removeBugSubscription(self.user) |
9 | + target.removeBugSubscription(self.user, self.user) |
10 | self.request.response.addNotification( |
11 | 'You have unsubscribed from "%s". You ' |
12 | 'will no longer automatically receive e-mail about ' |
13 | @@ -197,7 +197,7 @@ |
14 | team.displayname, self.context.displayname)) |
15 | |
16 | for team in subscriptions - form_selected_teams: |
17 | - target.removeBugSubscription(team) |
18 | + target.removeBugSubscription(team, self.user) |
19 | self.request.response.addNotification( |
20 | 'The %s team will no longer automatically receive ' |
21 | 'e-mail about changes to public bugs in "%s".' % ( |
22 | @@ -220,7 +220,7 @@ |
23 | |
24 | subscriptions_to_remove = data.get('remove_other_subscriptions', []) |
25 | for subscription in subscriptions_to_remove: |
26 | - target.removeBugSubscription(subscription) |
27 | + target.removeBugSubscription(subscription, self.user) |
28 | self.request.response.addNotification( |
29 | '%s will no longer automatically receive e-mail about ' |
30 | 'public bugs in "%s".' % ( |
31 | |
32 | === modified file 'lib/canonical/launchpad/database/structuralsubscription.py' |
33 | --- lib/canonical/launchpad/database/structuralsubscription.py 2009-07-23 13:44:13 +0000 |
34 | +++ lib/canonical/launchpad/database/structuralsubscription.py 2009-08-25 12:04:52 +0000 |
35 | @@ -5,6 +5,7 @@ |
36 | __all__ = ['StructuralSubscription', |
37 | 'StructuralSubscriptionTargetMixin'] |
38 | |
39 | +from zope.component import getUtility |
40 | from zope.interface import implements |
41 | |
42 | from sqlobject import ForeignKey |
43 | @@ -16,9 +17,10 @@ |
44 | |
45 | from canonical.launchpad.interfaces import ( |
46 | BlueprintNotificationLevel, BugNotificationLevel, DeleteSubscriptionError, |
47 | - IDistribution, IDistributionSourcePackage, IDistroSeries, IMilestone, |
48 | - IProduct, IProductSeries, IProject, IStructuralSubscription, |
49 | - IStructuralSubscriptionTarget) |
50 | + IDistribution, IDistributionSourcePackage, IDistroSeries, |
51 | + ILaunchpadCelebrities, IMilestone, IProduct, IProductSeries, IProject, |
52 | + IStructuralSubscription, IStructuralSubscriptionTarget, |
53 | + UserCannotSubscribePerson) |
54 | from lp.registry.interfaces.person import ( |
55 | validate_public_person, validate_person_not_private_membership) |
56 | |
57 | @@ -129,8 +131,35 @@ |
58 | '%s is not a valid structural subscription target.') |
59 | return args |
60 | |
61 | + def _userCanAlterSubscription(self, subscriber, subscribed_by): |
62 | + """Check if a user can change a subscription for a person.""" |
63 | + # A Launchpad administrator or the user can subscribe a user. |
64 | + # A Launchpad or team admin can subscribe a team. |
65 | + |
66 | + # Nobody else can, unless the context is a IDistributionSourcePackage, |
67 | + # in which case the drivers or owner can. |
68 | + if IDistributionSourcePackage.providedBy(self): |
69 | + for driver in self.distribution.drivers: |
70 | + if subscribed_by.inTeam(driver): |
71 | + return True |
72 | + if subscribed_by.inTeam(self.distribution.owner): |
73 | + return True |
74 | + |
75 | + admins = getUtility(ILaunchpadCelebrities).admin |
76 | + return (subscriber is subscribed_by or |
77 | + subscriber in subscribed_by.getAdministratedTeams() or |
78 | + subscribed_by.inTeam(admins)) |
79 | + |
80 | def addSubscription(self, subscriber, subscribed_by): |
81 | """See `IStructuralSubscriptionTarget`.""" |
82 | + if subscriber is None: |
83 | + subscriber = subscribed_by |
84 | + |
85 | + if not self._userCanAlterSubscription(subscriber, subscribed_by): |
86 | + raise UserCannotSubscribePerson( |
87 | + '%s does not have permission to subscribe %s.' % ( |
88 | + subscribed_by.name, subscriber.name)) |
89 | + |
90 | existing_subscription = self.getSubscription(subscriber) |
91 | |
92 | if existing_subscription is not None: |
93 | @@ -151,20 +180,28 @@ |
94 | sub.bug_notification_level = BugNotificationLevel.COMMENTS |
95 | return sub |
96 | |
97 | - def removeBugSubscription(self, person): |
98 | + def removeBugSubscription(self, subscriber, unsubscribed_by): |
99 | """See `IStructuralSubscriptionTarget`.""" |
100 | + if subscriber is None: |
101 | + subscriber = unsubscribed_by |
102 | + |
103 | + if not self._userCanAlterSubscription(subscriber, unsubscribed_by): |
104 | + raise UserCannotSubscribePerson( |
105 | + '%s does not have permission to unsubscribe %s.' % ( |
106 | + unsubscribed_by.name, subscriber.name)) |
107 | + |
108 | subscription_to_remove = None |
109 | for subscription in self.getSubscriptions( |
110 | min_bug_notification_level=BugNotificationLevel.METADATA): |
111 | # Only search for bug subscriptions |
112 | - if subscription.subscriber == person: |
113 | + if subscription.subscriber == subscriber: |
114 | subscription_to_remove = subscription |
115 | break |
116 | |
117 | if subscription_to_remove is None: |
118 | raise DeleteSubscriptionError( |
119 | "%s is not subscribed to %s." % ( |
120 | - person.name, self.displayname)) |
121 | + subscriber.name, self.displayname)) |
122 | else: |
123 | if (subscription_to_remove.blueprint_notification_level > |
124 | BlueprintNotificationLevel.NOTHING): |
125 | |
126 | === modified file 'lib/canonical/launchpad/doc/structural-subscriptions.txt' |
127 | --- lib/canonical/launchpad/doc/structural-subscriptions.txt 2009-04-17 10:32:16 +0000 |
128 | +++ lib/canonical/launchpad/doc/structural-subscriptions.txt 2009-08-25 11:08:42 +0000 |
129 | @@ -160,8 +160,8 @@ |
130 | |
131 | >>> evolution_sub.blueprint_notification_level = ( |
132 | ... BlueprintNotificationLevel.METADATA) |
133 | - >>> evolution_package.removeBugSubscription(sampleperson) |
134 | - >>> ubuntu.removeBugSubscription(sampleperson) |
135 | + >>> evolution_package.removeBugSubscription(sampleperson, sampleperson) |
136 | + >>> ubuntu.removeBugSubscription(sampleperson, sampleperson) |
137 | >>> syncUpdate(evolution_sub) |
138 | |
139 | Sample Person is no longer a subscriber to the package, but Foo Bar |
140 | |
141 | === modified file 'lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt' |
142 | --- lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt 2008-02-11 17:44:30 +0000 |
143 | +++ lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt 2009-08-25 11:08:42 +0000 |
144 | @@ -48,31 +48,100 @@ |
145 | >>> target.addBugSubscription(no_priv, no_priv) |
146 | <StructuralSubscription ...> |
147 | |
148 | -Let's add an ITeam as one of the subscribers: |
149 | +People can only be subscribed by themselves, and only the team admins may |
150 | +subscribe a team. |
151 | + |
152 | +no-priv, who has no relationship to ubuntu-team, cannot subscribe it. |
153 | |
154 | >>> ubuntu_team = personset.getByName("ubuntu-team") |
155 | - >>> target.addBugSubscription(ubuntu_team, ubuntu_team) |
156 | - <StructuralSubscription ...> |
157 | - |
158 | - >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
159 | - [u'no-priv', u'ubuntu-team'] |
160 | + >>> target.addBugSubscription(ubuntu_team, no_priv) |
161 | + Traceback (most recent call last): |
162 | + ... |
163 | + UserCannotSubscribePerson: no-priv does not have permission to subscribe ubuntu-team. |
164 | + |
165 | +But kamion, an admin of the team, can. |
166 | + |
167 | + >>> kamion = personset.getByName("kamion") |
168 | + >>> target.addBugSubscription(ubuntu_team, kamion) |
169 | + <StructuralSubscription ...> |
170 | + |
171 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
172 | + [u'no-priv', u'ubuntu-team'] |
173 | + |
174 | +foobar, a Launchpad administrator, can as well. |
175 | + |
176 | + >>> foobar = personset.getByName("name16") |
177 | + >>> target.addBugSubscription(ubuntu_team, foobar) |
178 | + <StructuralSubscription ...> |
179 | + |
180 | +A non-admin cannot subscribe a person other than themselves. |
181 | + |
182 | + >>> target.addBugSubscription(kamion, no_priv) |
183 | + Traceback (most recent call last): |
184 | + ... |
185 | + UserCannotSubscribePerson: no-priv does not have permission to subscribe kamion. |
186 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
187 | + [u'no-priv', u'ubuntu-team'] |
188 | + |
189 | +But again, an admin can. |
190 | + |
191 | + >>> target.addBugSubscription(kamion, foobar) |
192 | + <StructuralSubscription ...> |
193 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
194 | + [u'kamion', u'no-priv', u'ubuntu-team'] |
195 | |
196 | To remove a bug subscription, use |
197 | IStructuralSubscriptionTarget.removeBugSubscription: |
198 | |
199 | - >>> target.removeBugSubscription(no_priv) |
200 | - >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
201 | - [u'ubuntu-team'] |
202 | + >>> target.removeBugSubscription(no_priv, no_priv) |
203 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
204 | + [u'kamion', u'ubuntu-team'] |
205 | + |
206 | +The subscription rules apply to unsubscription as well. |
207 | + |
208 | +An unprivileged user cannot unsubscribe a team. |
209 | + |
210 | + >>> target.removeBugSubscription(ubuntu_team, no_priv) |
211 | + Traceback (most recent call last): |
212 | + ... |
213 | + UserCannotSubscribePerson: no-priv does not have permission to unsubscribe ubuntu-team. |
214 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
215 | + [u'kamion', u'ubuntu-team'] |
216 | + |
217 | +But a team admin can. |
218 | + |
219 | + >>> target.removeBugSubscription(ubuntu_team, kamion) |
220 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
221 | + [u'kamion'] |
222 | + |
223 | +An unprivileged user also cannot unsubscribe another user. |
224 | + |
225 | + >>> target.removeBugSubscription(kamion, no_priv) |
226 | + Traceback (most recent call last): |
227 | + ... |
228 | + UserCannotSubscribePerson: no-priv does not have permission to unsubscribe kamion. |
229 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
230 | + [u'kamion'] |
231 | + |
232 | +But the user themselves can. |
233 | + |
234 | + >>> target.removeBugSubscription(kamion, kamion) |
235 | + >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions]) |
236 | + [] |
237 | |
238 | Trying to remove a subscription that doesn't exist on a target raises a |
239 | DeleteSubscriptionError. |
240 | |
241 | - >>> foobar = personset.getByName("name16") |
242 | - >>> target.removeBugSubscription(foobar) |
243 | + >>> target.removeBugSubscription(foobar, foobar) |
244 | Traceback (most recent call last): |
245 | ... |
246 | DeleteSubscriptionError: ... |
247 | |
248 | +Let's subscribe ubuntu-team again. |
249 | + |
250 | + >>> target.addBugSubscription(ubuntu_team, foobar) |
251 | + <StructuralSubscription ...> |
252 | + |
253 | Trying to remove a bug subscription when notification levels for other |
254 | applications are set, doesn't remove the subscription. Instead the |
255 | notification level for bugs is set to NOTHING. |
256 | @@ -93,7 +162,7 @@ |
257 | >>> print_subscriptions_list(target.getSubscriptions()) |
258 | name16 COMMENTS LIFECYCLE |
259 | ubuntu-team COMMENTS NOTHING |
260 | - >>> target.removeBugSubscription(foobar) |
261 | + >>> target.removeBugSubscription(foobar, foobar) |
262 | >>> print_subscriptions_list(target.getSubscriptions()) |
263 | name16 NOTHING LIFECYCLE |
264 | ubuntu-team COMMENTS NOTHING |
265 | |
266 | === modified file 'lib/canonical/launchpad/interfaces/structuralsubscription.py' |
267 | --- lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-07-17 00:26:05 +0000 |
268 | +++ lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-08-25 11:08:42 +0000 |
269 | @@ -13,7 +13,8 @@ |
270 | 'DeleteSubscriptionError', |
271 | 'IStructuralSubscription', |
272 | 'IStructuralSubscriptionForm', |
273 | - 'IStructuralSubscriptionTarget' |
274 | + 'IStructuralSubscriptionTarget', |
275 | + 'UserCannotSubscribePerson', |
276 | ] |
277 | |
278 | from zope.interface import Attribute, Interface |
279 | @@ -165,7 +166,7 @@ |
280 | :return: The new bug subscription. |
281 | """ |
282 | |
283 | - def removeBugSubscription(subscriber): |
284 | + def removeBugSubscription(subscriber, unsubscribed_by): |
285 | """Remove a subscription to bugs from this structure. |
286 | |
287 | If subscription levels for other applications are set, |
288 | @@ -173,6 +174,7 @@ |
289 | `NOTHING`, otherwise, destroy the subscription. |
290 | |
291 | :subscriber: The IPerson who will be subscribed. |
292 | + :unsubscribed_by: The IPerson removing the subscription. |
293 | """ |
294 | |
295 | def getSubscription(person): |
296 | @@ -207,3 +209,7 @@ |
297 | |
298 | Raised when an error occurred trying to delete a |
299 | structural subscription.""" |
300 | + |
301 | + |
302 | +class UserCannotSubscribePerson(Exception): |
303 | + """User does not have permission to subscribe the person or team.""" |
304 | |
305 | === modified file 'lib/lp/bugs/doc/initial-bug-contacts.txt' |
306 | --- lib/lp/bugs/doc/initial-bug-contacts.txt 2009-08-13 15:12:16 +0000 |
307 | +++ lib/lp/bugs/doc/initial-bug-contacts.txt 2009-08-25 11:08:42 +0000 |
308 | @@ -60,7 +60,7 @@ |
309 | To remove a subscription, use |
310 | IStructuralSubscriptionTarget.removeBugSubscription: |
311 | |
312 | - >>> debian_firefox.removeBugSubscription(sample_person) |
313 | + >>> debian_firefox.removeBugSubscription(sample_person, sample_person) |
314 | >>> sorted([sub.subscriber.id for sub in debian_firefox.bug_subscriptions]) |
315 | [17] |
316 | |
317 | @@ -68,7 +68,7 @@ |
318 | DeleteSubscriptionError. |
319 | |
320 | >>> foobar = personset.getByName("name16") |
321 | - >>> debian_firefox.removeBugSubscription(foobar) |
322 | + >>> debian_firefox.removeBugSubscription(foobar, foobar) |
323 | Traceback (most recent call last): |
324 | ... |
325 | DeleteSubscriptionError: ... |
326 | |
327 | === modified file 'lib/lp/bugs/tests/has-bug-supervisor.txt' |
328 | --- lib/lp/bugs/tests/has-bug-supervisor.txt 2009-06-12 16:36:02 +0000 |
329 | +++ lib/lp/bugs/tests/has-bug-supervisor.txt 2009-08-25 11:08:42 +0000 |
330 | @@ -50,7 +50,7 @@ |
331 | |
332 | If no_priv unsubscribes, he is still set as the bug supervisor. |
333 | |
334 | - >>> target.removeBugSubscription(no_priv) |
335 | + >>> target.removeBugSubscription(no_priv, no_priv) |
336 | >>> print target.bug_supervisor.preferredemail.email |
337 | no-priv@canonical.com |
338 | >>> no_priv in bug.getIndirectSubscribers() |
= Summary =
Structural subscriptions want to be exported through the API. This is the third of four branches: moving access control into the model, while it currently just resides in the view.
== Proposed fix == criptionTarget. {add,remove} BugSubscription .
Implement access control in IStructuralSubs
== Implementation details == criptionTarget. addBugSubscript ion already took the person performing the action as an argument, but removeBugSubscr iption needed it to be added.
IStructuralSubs
addBugSubscription calls addSubscription, so access control was implemented there and in removeBugSubscr iption. I threw the privilege checking code into SST._userCanAlt erSubscriptionF orPerson.
The only other behavioral change is defaulting the subscriber in addSubscription, removeSubscription to subscribed_by. This will be useful for the API.
== Tests ==
$ bin/test -vvt structural- subscriptions. txt -t initial- bug-contacts. txt -t has-bug- supervisor. txt -t bugs-page.txt