Merge lp:~benji/launchpad/bug-580035 into lp:launchpad
- bug-580035
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Māris Fogels |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11392 |
Proposed branch: | lp:~benji/launchpad/bug-580035 |
Merge into: | lp:launchpad |
Diff against target: |
933 lines (+308/-105) 7 files modified
lib/canonical/launchpad/mail/errortemplates/old-signature.txt (+2/-0) lib/canonical/launchpad/mail/handlers.py (+9/-4) lib/canonical/launchpad/mail/helpers.py (+20/-1) lib/canonical/launchpad/mail/tests/test_handlers.py (+87/-1) lib/canonical/launchpad/mail/tests/test_helpers.py (+46/-1) lib/canonical/launchpad/utilities/gpghandler.py (+21/-32) lib/lp/bugs/tests/bugs-emailinterface.txt (+123/-66) |
To merge this branch: | bzr merge lp:~benji/launchpad/bug-580035 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Deryck Hodge (community) | code | Approve | |
Māris Fogels (community) | Approve | ||
Review via email: mp+32917@code.launchpad.net |
Commit message
Description of the change
Commands can be sent to bugs via GPG signed email. The commands don't contain
a nonce, so if someone were able to get ahold of an email from someone they
could resend it later (bug 580035).
This branch greatly reduces the effective window for such an attack by
rejecting any email message containing commands if the signature was generated
too long ago (more than 48 hours).
To help avoid attacks that arise from people accidentally having their clocks
set too far in the future and thus creating messages that can be reused for a
long time, the branch also rejects emails with commands that were signed more
than 10 minutes in the future.
This approach is suggested fix number 3 in the bug description.
I had a pre-implementation discussion with Gary and he had one with Francis.
The bug email tests had a natural place to add tests for this behavior. To run
the tests:
bin/test -ct bugs-emailinter
I fixed a large amount of lint (but I don't think it pollutes the diff too
much). Here's the lint report that I made go away:
Linting changed files:
lib/canonical
lib/canonical
lib/canonical
lib/canonical
lib/lp/
./lib/canonical
353: W191 indentation contains tabs
353: E101 indentation contains mixed spaces and tabs
353: Line contains a tab character.
./lib/canonical
78: E202 whitespace before ']'
138: E302 expected 2 blank lines, found 1
./lib/canonical
78: E211 whitespace before '('
288: E202 whitespace before ')'
470: E202 whitespace before '}'
./lib/lp/
79: source exceeds 78 characters.
341: source exceeds 78 characters.
575: narrative exceeds 78 characters.
967: source exceeds 78 characters.
1226: source exceeds 78 characters.
1230: source has bad indentation.
1447: want exceeds 78 characters.
1473: narrative exceeds 78 characters.
1489: want exceeds 78 characters.
1502: want exceeds 78 characters.
1894: source exceeds 78 characters.
1895: source exceeds 78 characters.
2344: narrative has trailing whitespace.
2583: narrative has trailing whitespace.
2584: narrative has trailing whitespace.
2585: narrative has trailing whitespace.
2696: source exceeds 78 characters.
2895: want exceeds 78 characters.
Deryck Hodge (deryck) wrote : | # |
Benji and I chatted on IRC. I would indeed prefer this be converted to a unit test. I would prefer it before it lands (best laid plans and all that... ;)), but I won't block on that point. Thanks for pointing that out, Maris!
Cheers,
deryck
Deryck Hodge (deryck) wrote : | # |
The updates to make this a unit test look good to me, Benji. Thanks!
Preview Diff
1 | === added file 'lib/canonical/launchpad/mail/errortemplates/old-signature.txt' |
2 | --- lib/canonical/launchpad/mail/errortemplates/old-signature.txt 1970-01-01 00:00:00 +0000 |
3 | +++ lib/canonical/launchpad/mail/errortemplates/old-signature.txt 2010-08-18 18:27:52 +0000 |
4 | @@ -0,0 +1,2 @@ |
5 | +The message you sent included commands to modify the %(context)s, but the |
6 | +signature was (apparently) generated too far in the past or future. |
7 | |
8 | === modified file 'lib/canonical/launchpad/mail/handlers.py' |
9 | --- lib/canonical/launchpad/mail/handlers.py 2009-07-24 12:54:07 +0000 |
10 | +++ lib/canonical/launchpad/mail/handlers.py 2010-08-18 18:27:52 +0000 |
11 | @@ -27,7 +27,8 @@ |
12 | BugEmailCommands, get_error_message) |
13 | from canonical.launchpad.mail.helpers import ( |
14 | ensure_not_weakly_authenticated, get_main_body, guess_bugtask, |
15 | - IncomingEmailError, parse_commands, reformat_wiki_text) |
16 | + IncomingEmailError, parse_commands, reformat_wiki_text, |
17 | + ensure_sane_signature_timestamp) |
18 | from lp.services.mail.sendmail import sendmail, simple_sendmail |
19 | from canonical.launchpad.mail.specexploder import get_spec_url_from_moin_mail |
20 | from canonical.launchpad.mailnotification import ( |
21 | @@ -62,15 +63,19 @@ |
22 | commands = self.getCommands(signed_msg) |
23 | user, host = to_addr.split('@') |
24 | add_comment_to_bug = False |
25 | + signature = signed_msg.signature |
26 | |
27 | try: |
28 | if len(commands) > 0: |
29 | - ensure_not_weakly_authenticated(signed_msg, 'bug report') |
30 | + CONTEXT = 'bug report' |
31 | + ensure_not_weakly_authenticated(signed_msg, CONTEXT) |
32 | + if signature is not None: |
33 | + ensure_sane_signature_timestamp(signature, CONTEXT) |
34 | |
35 | if user.lower() == 'new': |
36 | # A submit request. |
37 | commands.insert(0, BugEmailCommands.get('bug', ['new'])) |
38 | - if signed_msg.signature is None: |
39 | + if signature is None: |
40 | raise IncomingEmailError( |
41 | get_error_message('not-gpg-signed.txt')) |
42 | elif user.isdigit(): |
43 | @@ -345,7 +350,7 @@ |
44 | """ |
45 | if question.status in [ |
46 | QuestionStatus.OPEN, QuestionStatus.NEEDSINFO, |
47 | - QuestionStatus.ANSWERED]: |
48 | + QuestionStatus.ANSWERED]: |
49 | question.giveAnswer(message.owner, message) |
50 | else: |
51 | # In the other states, only a comment can be added. |
52 | |
53 | === modified file 'lib/canonical/launchpad/mail/helpers.py' |
54 | --- lib/canonical/launchpad/mail/helpers.py 2010-08-16 13:50:28 +0000 |
55 | +++ lib/canonical/launchpad/mail/helpers.py 2010-08-18 18:27:52 +0000 |
56 | @@ -5,6 +5,7 @@ |
57 | |
58 | import os.path |
59 | import re |
60 | +import time |
61 | |
62 | from zope.component import getUtility |
63 | |
64 | @@ -74,7 +75,9 @@ |
65 | <...IDistroSeriesBugTask> |
66 | """ |
67 | bugtask_interfaces = [ |
68 | - IUpstreamBugTask, IDistroBugTask, IDistroSeriesBugTask |
69 | + IUpstreamBugTask, |
70 | + IDistroBugTask, |
71 | + IDistroSeriesBugTask, |
72 | ] |
73 | for interface in bugtask_interfaces: |
74 | if interface.providedBy(bugtask): |
75 | @@ -134,6 +137,7 @@ |
76 | |
77 | return text |
78 | |
79 | + |
80 | def parse_commands(content, command_names): |
81 | """Extract indented commands from email body. |
82 | |
83 | @@ -220,3 +224,18 @@ |
84 | no_key_template, import_url=import_url, |
85 | context=context) |
86 | raise IncomingEmailError(error_message) |
87 | + |
88 | + |
89 | +def ensure_sane_signature_timestamp(signature, context, |
90 | + error_template='old-signature.txt'): |
91 | + """Ensure the signature was generated recently but not in the future.""" |
92 | + fourty_eight_hours = 48 * 60 * 60 |
93 | + ten_minutes = 10 * 60 |
94 | + now = time.time() |
95 | + fourty_eight_hours_ago = now - fourty_eight_hours |
96 | + ten_minutes_in_the_future = now + ten_minutes |
97 | + |
98 | + if (signature.timestamp < fourty_eight_hours_ago |
99 | + or signature.timestamp > ten_minutes_in_the_future): |
100 | + error_message = get_error_message(error_template, context=context) |
101 | + raise IncomingEmailError(error_message) |
102 | |
103 | === modified file 'lib/canonical/launchpad/mail/tests/test_handlers.py' |
104 | --- lib/canonical/launchpad/mail/tests/test_handlers.py 2010-07-14 14:11:15 +0000 |
105 | +++ lib/canonical/launchpad/mail/tests/test_handlers.py 2010-08-18 18:27:52 +0000 |
106 | @@ -3,14 +3,18 @@ |
107 | |
108 | __metaclass__ = type |
109 | |
110 | +import email |
111 | +import time |
112 | import unittest |
113 | |
114 | from doctest import DocTestSuite |
115 | |
116 | +from canonical.database.sqlbase import commit |
117 | from canonical.launchpad.mail.commands import BugEmailCommand |
118 | from canonical.launchpad.mail.handlers import MaloneHandler |
119 | -from lp.testing import TestCaseWithFactory |
120 | from canonical.testing import LaunchpadFunctionalLayer |
121 | +from lp.services.mail import stub |
122 | +from lp.testing import TestCaseWithFactory, person_logged_in |
123 | |
124 | |
125 | class TestMaloneHandler(TestCaseWithFactory): |
126 | @@ -35,6 +39,88 @@ |
127 | self.assertEqual(['foo'], commands[0].string_args) |
128 | |
129 | |
130 | +class FakeSignature: |
131 | + |
132 | + def __init__(self, timestamp): |
133 | + self.timestamp = timestamp |
134 | + |
135 | + |
136 | +def get_last_email(): |
137 | + from_addr, to_addrs, raw_message = stub.test_emails[-1] |
138 | + sent_msg = email.message_from_string(raw_message) |
139 | + error_mail, original_mail = sent_msg.get_payload() |
140 | + # clear the emails so we don't accidentally get one from a previous test |
141 | + return dict( |
142 | + subject=sent_msg['Subject'], |
143 | + body=error_mail.get_payload(decode=True)) |
144 | + |
145 | + |
146 | +BAD_SIGNATURE_TIMESTAMP_MESSAGE = ( |
147 | + 'The message you sent included commands to modify the bug ' |
148 | + 'report, but the\nsignature was (apparently) generated too far ' |
149 | + 'in the past or future.') |
150 | + |
151 | + |
152 | +class TestSignatureTimestampValidation(TestCaseWithFactory): |
153 | + """GPG signature timestamps are checked for emails containing commands.""" |
154 | + |
155 | + layer = LaunchpadFunctionalLayer |
156 | + |
157 | + def test_far_past_gpg_signature_timestamp(self): |
158 | + # If an email message's GPG signature's timestamp is too far in the |
159 | + # past and the message contains a command, the command will fail and |
160 | + # send the user an email telling them what happened. |
161 | + |
162 | + msg = self.factory.makeSignedMessage(body=' security no') |
163 | + now = time.time() |
164 | + one_week = 60 * 60 * 24 * 7 |
165 | + msg.signature = FakeSignature(timestamp=now-one_week) |
166 | + handler = MaloneHandler() |
167 | + with person_logged_in(self.factory.makePerson()): |
168 | + success = handler.process(msg, msg['To']) |
169 | + commit() |
170 | + email = get_last_email() |
171 | + self.assertEqual(email['subject'], 'Submit Request Failure') |
172 | + self.assertIn(BAD_SIGNATURE_TIMESTAMP_MESSAGE, email['body']) |
173 | + |
174 | + def test_far_future_gpg_signature_timestamp(self): |
175 | + # If an email message's GPG signature's timestamp is too far in the |
176 | + # future and the message contains a command, the command will fail and |
177 | + # send the user an email telling them what happened. |
178 | + |
179 | + msg = self.factory.makeSignedMessage(body=' security no') |
180 | + now = time.time() |
181 | + one_week = 60 * 60 * 24 * 7 |
182 | + msg.signature = FakeSignature(timestamp=now+one_week) |
183 | + handler = MaloneHandler() |
184 | + with person_logged_in(self.factory.makePerson()): |
185 | + success = handler.process(msg, msg['To']) |
186 | + commit() |
187 | + email = get_last_email() |
188 | + self.assertEqual(email['subject'], 'Submit Request Failure') |
189 | + self.assertIn(BAD_SIGNATURE_TIMESTAMP_MESSAGE, email['body']) |
190 | + |
191 | + def test_bad_timestamp_but_no_commands(self): |
192 | + # If an email message's GPG signature's timestamp is too far in the |
193 | + # future or past but it doesn't contain any commands, the email is |
194 | + # processed anyway. |
195 | + |
196 | + msg = self.factory.makeSignedMessage( |
197 | + body='I really hope this bug gets fixed.') |
198 | + now = time.time() |
199 | + one_week = 60 * 60 * 24 * 7 |
200 | + msg.signature = FakeSignature(timestamp=now+one_week) |
201 | + handler = MaloneHandler() |
202 | + # Clear old emails before potentially generating more. |
203 | + del stub.test_emails[:] |
204 | + with person_logged_in(self.factory.makePerson()): |
205 | + success = handler.process(msg, msg['To']) |
206 | + commit() |
207 | + # Since there were no commands in the poorly-timestamped message, no |
208 | + # error emails were generated. |
209 | + self.assertEqual(stub.test_emails, []) |
210 | + |
211 | + |
212 | def test_suite(): |
213 | suite = unittest.TestSuite() |
214 | suite.addTests(DocTestSuite('canonical.launchpad.mail.handlers')) |
215 | |
216 | === modified file 'lib/canonical/launchpad/mail/tests/test_helpers.py' |
217 | --- lib/canonical/launchpad/mail/tests/test_helpers.py 2010-07-14 14:11:15 +0000 |
218 | +++ lib/canonical/launchpad/mail/tests/test_helpers.py 2010-08-18 18:27:52 +0000 |
219 | @@ -4,6 +4,7 @@ |
220 | __metaclass__ = type |
221 | |
222 | from doctest import DocTestSuite |
223 | +import time |
224 | import unittest |
225 | |
226 | from zope.interface import directlyProvides, directlyProvidedBy |
227 | @@ -12,7 +13,7 @@ |
228 | EmailProcessingError, IWeaklyAuthenticatedPrincipal) |
229 | from canonical.launchpad.mail.helpers import ( |
230 | ensure_not_weakly_authenticated, get_person_or_team, |
231 | - IncomingEmailError, parse_commands) |
232 | + IncomingEmailError, parse_commands, ensure_sane_signature_timestamp) |
233 | from lp.testing import login_person, TestCase, TestCaseWithFactory |
234 | from canonical.testing import DatabaseFunctionalLayer |
235 | from canonical.launchpad.webapp.interaction import get_current_principal |
236 | @@ -77,6 +78,50 @@ |
237 | parse_commands(' command:', ['command'])) |
238 | |
239 | |
240 | +class FakeSignature: |
241 | + |
242 | + def __init__(self, timestamp): |
243 | + self.timestamp = timestamp |
244 | + |
245 | + |
246 | +class TestEnsureSaneSignatureTimestamp(unittest.TestCase): |
247 | + """Tests for ensure_sane_signature_timestamp""" |
248 | + |
249 | + def test_too_old_timestamp(self): |
250 | + # signature timestamps shouldn't be too old |
251 | + now = time.time() |
252 | + one_week = 60 * 60 * 24 * 7 |
253 | + signature = FakeSignature(timestamp=now-one_week) |
254 | + self.assertRaises( |
255 | + IncomingEmailError, ensure_sane_signature_timestamp, |
256 | + signature, 'bug report') |
257 | + |
258 | + def test_future_timestamp(self): |
259 | + # signature timestamps shouldn't be (far) in the future |
260 | + now = time.time() |
261 | + one_week = 60 * 60 * 24 * 7 |
262 | + signature = FakeSignature(timestamp=now+one_week) |
263 | + self.assertRaises( |
264 | + IncomingEmailError, ensure_sane_signature_timestamp, |
265 | + signature, 'bug report') |
266 | + |
267 | + def test_near_future_timestamp(self): |
268 | + # signature timestamps in the near future are OK |
269 | + now = time.time() |
270 | + one_minute = 60 |
271 | + signature = FakeSignature(timestamp=now+one_minute) |
272 | + # this should not raise an exception |
273 | + ensure_sane_signature_timestamp(signature, 'bug report') |
274 | + |
275 | + def test_recent_timestamp(self): |
276 | + # signature timestamps in the recent past are OK |
277 | + now = time.time() |
278 | + one_hour = 60 * 60 |
279 | + signature = FakeSignature(timestamp=now-one_hour) |
280 | + # this should not raise an exception |
281 | + ensure_sane_signature_timestamp(signature, 'bug report') |
282 | + |
283 | + |
284 | class TestEnsureNotWeaklyAuthenticated(TestCaseWithFactory): |
285 | """Test the ensure_not_weakly_authenticated function.""" |
286 | |
287 | |
288 | === modified file 'lib/canonical/launchpad/utilities/gpghandler.py' |
289 | --- lib/canonical/launchpad/utilities/gpghandler.py 2010-08-10 16:17:12 +0000 |
290 | +++ lib/canonical/launchpad/utilities/gpghandler.py 2010-08-18 18:27:52 +0000 |
291 | @@ -75,9 +75,9 @@ |
292 | # automatically retrieve from the keyserver unknown key when |
293 | # verifying signatures and 'no-auto-check-trustdb' avoid wasting |
294 | # time verifying the local keyring consistence. |
295 | - conf.write ('keyserver hkp://%s\n' |
296 | - 'keyserver-options auto-key-retrieve\n' |
297 | - 'no-auto-check-trustdb\n' % config.gpghandler.host) |
298 | + conf.write('keyserver hkp://%s\n' |
299 | + 'keyserver-options auto-key-retrieve\n' |
300 | + 'no-auto-check-trustdb\n' % config.gpghandler.host) |
301 | conf.close() |
302 | # create a local atexit handler to remove the configuration directory |
303 | # on normal termination. |
304 | @@ -156,35 +156,26 @@ |
305 | sig = StringIO(signature) |
306 | # store the content |
307 | plain = StringIO(content) |
308 | - # process it |
309 | - try: |
310 | - signatures = ctx.verify(sig, plain, None) |
311 | - except gpgme.GpgmeError, e: |
312 | - # XXX: 2010-04-26, Salgado, bug=570244: This hack is needed |
313 | - # for python2.5 compatibility. We should remove it when we no |
314 | - # longer need to run on python2.5. |
315 | - if hasattr(e, 'strerror'): |
316 | - msg = e.strerror |
317 | - else: |
318 | - msg = e.message |
319 | - raise GPGVerificationError(msg) |
320 | + args = (sig, plain, None) |
321 | else: |
322 | # store clearsigned signature |
323 | sig = StringIO(content) |
324 | # writeable content |
325 | plain = StringIO() |
326 | - # process it |
327 | - try: |
328 | - signatures = ctx.verify(sig, None, plain) |
329 | - except gpgme.GpgmeError, e: |
330 | - # XXX: 2010-04-26, Salgado, bug=570244: This hack is needed |
331 | - # for python2.5 compatibility. We should remove it when we no |
332 | - # longer need to run on python2.5. |
333 | - if hasattr(e, 'strerror'): |
334 | - msg = e.strerror |
335 | - else: |
336 | - msg = e.message |
337 | - raise GPGVerificationError(msg) |
338 | + args = (sig, None, plain) |
339 | + |
340 | + # process it |
341 | + try: |
342 | + signatures = ctx.verify(*args) |
343 | + except gpgme.GpgmeError, e: |
344 | + # XXX: 2010-04-26, Salgado, bug=570244: This hack is needed |
345 | + # for python2.5 compatibility. We should remove it when we no |
346 | + # longer need to run on python2.5. |
347 | + if hasattr(e, 'strerror'): |
348 | + msg = e.strerror |
349 | + else: |
350 | + msg = e.message |
351 | + raise GPGVerificationError(msg) |
352 | |
353 | # XXX jamesh 2006-01-31: |
354 | # We raise an exception if we don't get exactly one signature. |
355 | @@ -204,7 +195,6 @@ |
356 | 'found multiple signatures') |
357 | |
358 | signature = signatures[0] |
359 | - |
360 | # signature.status == 0 means "Ok" |
361 | if signature.status is not None: |
362 | raise GPGVerificationError(signature.status.args) |
363 | @@ -295,8 +285,7 @@ |
364 | # See more information at: |
365 | # http://pyme.sourceforge.net/doc/gpgme/Generating-Keys.html |
366 | result = context.genkey( |
367 | - signing_only_param % {'name': name.encode('utf-8')} |
368 | - ) |
369 | + signing_only_param % {'name': name.encode('utf-8')}) |
370 | |
371 | # Right, it might seem paranoid to have this many assertions, |
372 | # but we have to take key generation very seriously. |
373 | @@ -477,8 +466,8 @@ |
374 | """See IGPGHandler""" |
375 | params = { |
376 | 'search': '0x%s' % fingerprint, |
377 | - 'op': action |
378 | - } |
379 | + 'op': action, |
380 | + } |
381 | if public: |
382 | host = config.gpghandler.public_host |
383 | else: |
384 | |
385 | === modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt' |
386 | --- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-08-02 17:48:13 +0000 |
387 | +++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-08-18 18:27:52 +0000 |
388 | @@ -1,11 +1,13 @@ |
389 | -= Launchpad Bugs e-mail interface = |
390 | +Launchpad Bugs e-mail interface |
391 | +=============================== |
392 | |
393 | Launchpad's bugtracker has an e-mail interface, with which you may report new |
394 | bugs, add comments, and change the details of existing bug reports. Commands |
395 | can be interleaved within a comment, so to distinguish them from the comment, |
396 | they must be indented with at least one space or tab character. |
397 | |
398 | -== Submit a new bug == |
399 | +Submit a new bug |
400 | +---------------- |
401 | |
402 | To report a bug, you send an OpenPGP-signed e-mail message to |
403 | new@bugs.launchpad-domain. You must have registered your key in |
404 | @@ -48,12 +50,19 @@ |
405 | signed, so that the system can verify the sender. But to avoid having |
406 | to sign each email, we'll create a class which fakes a signed email: |
407 | |
408 | - >>> import email.Utils |
409 | + >>> import time |
410 | + >>> class MockSignature(object): |
411 | + ... def __init__(self): |
412 | + ... self.timestamp = time.time() |
413 | + |
414 | + >>> import email.Message |
415 | >>> class MockSignedMessage(email.Message.Message): |
416 | + ... def __init__(self, *args, **kws): |
417 | + ... email.Message.Message.__init__(self, *args, **kws) |
418 | + ... self.signature = MockSignature() |
419 | ... @property |
420 | ... def signedMessage(self): |
421 | ... return self |
422 | - ... signature = object() |
423 | |
424 | And since we'll pass the email directly to the correct handler, |
425 | we'll have to authenticate the user manually: |
426 | @@ -66,10 +75,15 @@ |
427 | |
428 | >>> from canonical.launchpad.mail.handlers import MaloneHandler |
429 | >>> handler = MaloneHandler() |
430 | - >>> def process_email(raw_mail): |
431 | - ... msg = email.message_from_string(raw_mail, _class=MockSignedMessage) |
432 | + >>> def construct_email(raw_mail): |
433 | + ... msg = email.message_from_string( |
434 | + ... raw_mail, _class=MockSignedMessage) |
435 | ... if not msg.has_key('Message-Id'): |
436 | ... msg['Message-Id'] = factory.makeUniqueRFC822MsgId() |
437 | + ... return msg |
438 | + |
439 | + >>> def process_email(raw_mail): |
440 | + ... msg = construct_email(raw_mail) |
441 | ... handler.process(msg, msg['To']) |
442 | |
443 | >>> process_email(submit_mail) |
444 | @@ -224,7 +238,8 @@ |
445 | u'A folded email subject' |
446 | |
447 | |
448 | -== Add a comment == |
449 | +Add a comment |
450 | +------------- |
451 | |
452 | After a bug has been submitted a notification is sent out. The reply-to |
453 | address is set to the bug address, $bugid@malone-domain. We can send |
454 | @@ -263,7 +278,8 @@ |
455 | True |
456 | |
457 | |
458 | -== Edit bugs == |
459 | +Edit bugs |
460 | +--------- |
461 | |
462 | Sometimes you may want to simply edit a bug, without adding a comment. |
463 | For that you can send mails to edit@malone-domain. |
464 | @@ -304,7 +320,8 @@ |
465 | Nicer summary |
466 | |
467 | |
468 | -== GPG signing and adding comments == |
469 | +GPG signing and adding comments |
470 | +------------------------------- |
471 | |
472 | In order to include commands in the comment, the email has to be GPG |
473 | signed. The key used to sign the email has to be associated with the |
474 | @@ -322,7 +339,8 @@ |
475 | will provide IWeaklyAuthenticatedPrincipal. Let's mark the current |
476 | principal with that. |
477 | |
478 | - >>> from canonical.launchpad.interfaces import IWeaklyAuthenticatedPrincipal |
479 | + >>> from canonical.launchpad.interfaces import ( |
480 | + ... IWeaklyAuthenticatedPrincipal) |
481 | >>> from zope.interface import directlyProvides, directlyProvidedBy |
482 | >>> from zope.security.management import queryInteraction |
483 | >>> participations = queryInteraction().participations |
484 | @@ -446,13 +464,14 @@ |
485 | ... provided_interfaces - IWeaklyAuthenticatedPrincipal) |
486 | |
487 | |
488 | -== Commands == |
489 | +Commands |
490 | +-------- |
491 | |
492 | Now let's take a closer look at all the commands that are available for |
493 | us to play with. First we define a function to easily submit commands |
494 | to edit bug 4: |
495 | |
496 | - >>> def submit_commands(bug, *commands): |
497 | + >>> def construct_command_email(bug, *commands): |
498 | ... edit_mail = ("From: test@canonical.com\n" |
499 | ... "To: edit@malone-domain\n" |
500 | ... "Date: Fri Jun 17 10:10:23 BST 2005\n" |
501 | @@ -460,12 +479,20 @@ |
502 | ... "\n" |
503 | ... " bug %d\n" % bug.id) |
504 | ... edit_mail += ' ' + '\n '.join(commands) |
505 | - ... process_email(edit_mail) |
506 | + ... return construct_email(edit_mail) |
507 | + |
508 | + >>> def submit_command_email(msg): |
509 | + ... handler.process(msg, msg['To']) |
510 | ... commit() |
511 | ... sync(bug) |
512 | |
513 | - |
514 | -=== bug $bugid === |
515 | + >>> def submit_commands(bug, *commands): |
516 | + ... msg = construct_command_email(bug, *commands) |
517 | + ... submit_command_email(msg) |
518 | + |
519 | + |
520 | +bug $bugid |
521 | +~~~~~~~~~~ |
522 | |
523 | Switches what bug you want to edit. Example: |
524 | |
525 | @@ -509,7 +536,8 @@ |
526 | ... |
527 | |
528 | |
529 | -=== summary "$summary" === |
530 | +summary "$summary" |
531 | +~~~~~~~~~~~~~~~~~~ |
532 | |
533 | Changes the summary of the bug. The title has to be enclosed in |
534 | quotes. Example: |
535 | @@ -541,12 +569,13 @@ |
536 | ... |
537 | |
538 | |
539 | -=== private yes|no === |
540 | +private yes|no |
541 | +~~~~~~~~~~~~~~ |
542 | |
543 | Changes the visibility of the bug. Example: |
544 | |
545 | -(We'll subscribe Sample Person to this bug before marking it private, otherwise |
546 | -permission to complete the operation will be denied.) |
547 | +(We'll subscribe Sample Person to this bug before marking it private, |
548 | +otherwise permission to complete the operation will be denied.) |
549 | |
550 | >>> subscription = bug_four.subscribe(bug_four.owner, bug_four.owner) |
551 | |
552 | @@ -598,7 +627,8 @@ |
553 | ... |
554 | |
555 | |
556 | -=== security yes|no === |
557 | +security yes|no |
558 | +~~~~~~~~~~~~~~~ |
559 | |
560 | Changes the security flag of the bug. Example: |
561 | |
562 | @@ -647,7 +677,8 @@ |
563 | security yes |
564 | ... |
565 | |
566 | -=== subscribe [$name|$email] === |
567 | +subscribe [$name|$email] |
568 | +~~~~~~~~~~~~~~~~~~~~~~~~ |
569 | |
570 | Subscribes yourself or someone else to the bug. All arguments are |
571 | optional. If you don't specify a name, the sender of the email will |
572 | @@ -688,7 +719,8 @@ |
573 | non_existant@canonical.com |
574 | ... |
575 | |
576 | -=== unsubscribe [$name|$email] === |
577 | +unsubscribe [$name|$email] |
578 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
579 | |
580 | Unsubscribes yourself or someone else from the bug. If you don't |
581 | specify a name or email, the sender of the email will be |
582 | @@ -782,7 +814,8 @@ |
583 | >>> submit_commands(bug_four, 'subscribe test@canonical.com') |
584 | |
585 | |
586 | -=== tag $tag === |
587 | +tag $tag |
588 | +~~~~~~~~ |
589 | |
590 | The 'tag' command assigns a tag to a bug. Using this command we will add the |
591 | tags foo and bar to the bug. Adding a single tag multiple times should |
592 | @@ -865,7 +898,8 @@ |
593 | with-hyphen+period. |
594 | |
595 | |
596 | -=== duplicate $bug_id === |
597 | +duplicate $bug_id |
598 | +~~~~~~~~~~~~~~~~~ |
599 | |
600 | The 'duplicate' command marks a bug as a duplicate of another bug. |
601 | |
602 | @@ -927,11 +961,13 @@ |
603 | ... |
604 | |
605 | |
606 | -=== cve $cve === |
607 | +cve $cve |
608 | +~~~~~~~~ |
609 | |
610 | The 'cve' command associates a bug with a CVE reference. |
611 | |
612 | - >>> from canonical.launchpad.interfaces import CreateBugParams, IProductSet |
613 | + >>> from canonical.launchpad.interfaces import (CreateBugParams, |
614 | + ... IProductSet) |
615 | >>> def new_firefox_bug(): |
616 | ... firefox = getUtility(IProductSet).getByName('firefox') |
617 | ... return firefox.createBug(CreateBugParams( |
618 | @@ -962,7 +998,8 @@ |
619 | Launchpad can't find the CVE "no-such-cve". |
620 | ... |
621 | |
622 | -=== affects, assignee, status, importance, milestone === |
623 | +affects, assignee, status, importance, milestone |
624 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
625 | |
626 | affects $path [assignee $name|$email|nobody] |
627 | [status $status] |
628 | @@ -1189,11 +1226,12 @@ |
629 | >>> LaunchpadZopelessLayer.switchDbUser('launchpad') |
630 | >>> login('foo.bar@canonical.com') |
631 | >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
632 | - >>> ubuntu.driver = getUtility(IPersonSet).getByEmail('test@canonical.com') |
633 | + >>> ubuntu.driver = getUtility(IPersonSet).getByEmail( |
634 | + ... 'test@canonical.com') |
635 | >>> commit() |
636 | >>> LaunchpadZopelessLayer.switchDbUser(config.processmail.dbuser) |
637 | >>> login('test@canonical.com') |
638 | - >>> sync(bug) |
639 | + >>> sync(bug) |
640 | |
641 | Now a new bugtask for the series will be create directly. |
642 | |
643 | @@ -1328,7 +1366,8 @@ |
644 | Sample Person |
645 | |
646 | |
647 | -=== Restricted bug statuses === |
648 | +Restricted bug statuses |
649 | +~~~~~~~~~~~~~~~~~~~~~~~ |
650 | |
651 | >>> email_user = getUtility(ILaunchBag).user |
652 | |
653 | @@ -1409,7 +1448,8 @@ |
654 | status foo |
655 | ... |
656 | The 'status' command expects any of the following arguments: |
657 | - new, incomplete, opinion, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased |
658 | + new, incomplete, opinion, invalid, wontfix, expired, confirmed, triaged, |
659 | + inprogress, fixcommitted, fixreleased |
660 | <BLANKLINE> |
661 | For example: |
662 | <BLANKLINE> |
663 | @@ -1435,8 +1475,8 @@ |
664 | importance critical |
665 | ... |
666 | |
667 | -XXX mpt 20060516: "importance undecided" is a silly example, but customizing it |
668 | -to a realistic value is difficult (see convertArguments in |
669 | +XXX mpt 20060516: "importance undecided" is a silly example, but customizing |
670 | +it to a realistic value is difficult (see convertArguments in |
671 | launchpad/mail/commands.py). |
672 | |
673 | Trying to use the obsolete "severity" or "priority" commands: |
674 | @@ -1451,8 +1491,8 @@ |
675 | Failing command: |
676 | severity major |
677 | ... |
678 | - To make life a little simpler, Malone no longer has "priority" and "severity" |
679 | - fields. There is now an "importance" field... |
680 | + To make life a little simpler, Malone no longer has "priority" and |
681 | + "severity" fields. There is now an "importance" field... |
682 | ... |
683 | |
684 | >>> submit_commands(bug_four, 'affects firefox', 'priority low') |
685 | @@ -1464,8 +1504,8 @@ |
686 | Failing command: |
687 | priority low |
688 | ... |
689 | - To make life a little simpler, Malone no longer has "priority" and "severity" |
690 | - fields. There is now an "importance" field... |
691 | + To make life a little simpler, Malone no longer has "priority" and |
692 | + "severity" fields. There is now an "importance" field... |
693 | ... |
694 | |
695 | Invalid assignee: |
696 | @@ -1486,7 +1526,8 @@ |
697 | >>> stub.test_emails = [] |
698 | |
699 | |
700 | -== Multiple Commands == |
701 | +Multiple Commands |
702 | +----------------- |
703 | |
704 | An email can contain multiple commands, even for different bugs. |
705 | |
706 | @@ -1541,7 +1582,8 @@ |
707 | >>> bugtask_created_listener.unregister() |
708 | |
709 | |
710 | -== Default 'affects' target == |
711 | +Default 'affects' target |
712 | +------------------------ |
713 | |
714 | Most of the time it's not necessary to give the 'affects' command. If |
715 | you omit it, the email interface tries to guess which bug task you |
716 | @@ -1571,7 +1613,8 @@ |
717 | the user wanted to edit. We apply the following heuristics for choosing |
718 | which bug task to edit: |
719 | |
720 | -=== The user is a bug supervisors of the upstream product === |
721 | +The user is a bug supervisors of the upstream product |
722 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
723 | |
724 | >>> login('test@canonical.com') |
725 | >>> bug_one = getUtility(IBugSet).get(1) |
726 | @@ -1598,7 +1641,8 @@ |
727 | Status: New => Confirmed... |
728 | |
729 | |
730 | -=== The user is a package bug supervisor === |
731 | +The user is a package bug supervisor |
732 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
733 | |
734 | >>> from canonical.launchpad.interfaces import ( |
735 | ... IDistributionSet, ISourcePackageNameSet) |
736 | @@ -1638,12 +1682,14 @@ |
737 | ** Changed in: mozilla-firefox (Ubuntu) |
738 | Status: New => Confirmed |
739 | |
740 | -=== The user is a bug supervisor of a distribution === |
741 | +The user is a bug supervisor of a distribution |
742 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
743 | |
744 | XXX: TBD after InitialBugContacts is implemented. |
745 | -- Bjorn Tillenius, 2005-11-30 |
746 | |
747 | -=== The user is a distribution member === |
748 | +The user is a distribution member |
749 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
750 | |
751 | >>> login('foo.bar@canonical.com') |
752 | >>> submit_commands( |
753 | @@ -1668,7 +1714,8 @@ |
754 | Status: Confirmed => New |
755 | |
756 | |
757 | -=== No matching bug task === |
758 | +No matching bug task |
759 | +~~~~~~~~~~~~~~~~~~~~ |
760 | |
761 | If none of the bug tasks can be chosen, an error message is sent to the |
762 | user, telling him that he has to use the 'affects' command. |
763 | @@ -1696,7 +1743,8 @@ |
764 | ... |
765 | |
766 | |
767 | -== More About Error Handling == |
768 | +More About Error Handling |
769 | +------------------------- |
770 | |
771 | If an error is encountered, an email is sent to the sender informing |
772 | him about the error. Let's start with trying to submit a bug without |
773 | @@ -1706,6 +1754,7 @@ |
774 | |
775 | >>> from canonical.launchpad.mail import signed_message_from_string |
776 | >>> msg = signed_message_from_string(submit_mail) |
777 | + >>> import email.Utils |
778 | >>> msg['Message-Id'] = email.Utils.make_msgid() |
779 | >>> handler.process(msg, msg['To']) |
780 | True |
781 | @@ -1788,9 +1837,10 @@ |
782 | ... Subject: A bug with no affects |
783 | ... |
784 | ... I'm abusing ltsp-build-client to build a diskless fat client, but dint |
785 | - ... of --late-packages ubuntu-desktop. The dpkg --configure step for eg. HAL |
786 | - ... will try to start the daemon and failing, due to the lack of /proc. This |
787 | - ... is just the tip of the iceberg; I'll file more bugs as I go along. |
788 | + ... of --late-packages ubuntu-desktop. The dpkg --configure step for eg. |
789 | + ... HAL will try to start the daemon and failing, due to the lack of |
790 | + ... /proc. This is just the tip of the iceberg; I'll file more bugs as I |
791 | + ... go along. |
792 | ... """ |
793 | |
794 | >>> process_email(submit_mail) |
795 | @@ -1982,7 +2032,8 @@ |
796 | Original message body. |
797 | |
798 | |
799 | -== Error handling == |
800 | +Error handling |
801 | +-------------- |
802 | |
803 | When creating a new task and assigning it to a team, it is possible |
804 | that the team will not have a contact address. This is not generally |
805 | @@ -2023,7 +2074,8 @@ |
806 | landscape-developers |
807 | |
808 | |
809 | -== Recovering from errors == |
810 | +Recovering from errors |
811 | +---------------------- |
812 | |
813 | When a user sends an email with multiple commands, some of them might |
814 | fail (because of bad arguments, for example). Some commands, namely |
815 | @@ -2139,7 +2191,8 @@ |
816 | or send an email to help@launchpad.net |
817 | |
818 | |
819 | -== Terminating command input == |
820 | +Terminating command input |
821 | +------------------------- |
822 | |
823 | To make it possible to submit emails with lines that look like commands |
824 | (but aren't), a 'done' statement is provided. When the email parser |
825 | @@ -2176,7 +2229,8 @@ |
826 | CONFIRMED |
827 | |
828 | |
829 | -== Requesting help == |
830 | +Requesting help |
831 | +--------------- |
832 | |
833 | It's possible to ask for the help document for the email interface via |
834 | email too. Just send an email to `help@bugs.launchpad.net`. |
835 | @@ -2231,10 +2285,11 @@ |
836 | See you in {{{#launchpad}}}. |
837 | |
838 | |
839 | -== Email attachments == |
840 | +Email attachments |
841 | +----------------- |
842 | |
843 | -Email attachments are stored as bug attachments (provided that they |
844 | -match the criteria described below). |
845 | +Email attachments are stored as bug attachments (provided that they match the |
846 | +criteria described below). |
847 | |
848 | >>> def print_attachments(attachments): |
849 | ... if attachments.count() == 0: |
850 | @@ -2479,9 +2534,9 @@ |
851 | >>> process_email(submit_mail) |
852 | >>> print_attachments(get_latest_added_bug().attachments) |
853 | No attachments |
854 | - |
855 | -If an attachment has one of the content types application/applefile |
856 | -(the resource fork of a MacOS file), application/pgp-signature, |
857 | + |
858 | +If an attachment has one of the content types application/applefile |
859 | +(the resource fork of a MacOS file), application/pgp-signature, |
860 | application/pkcs7-signature, application/x-pkcs7-signature, |
861 | text/x-vcard, application/ms-tnef, it is not stored. |
862 | |
863 | @@ -2504,7 +2559,7 @@ |
864 | ... |
865 | ... -----BEGIN PGP SIGNATURE----- |
866 | ... Version: GnuPG v1.4.6 (GNU/Linux) |
867 | - ... |
868 | + ... |
869 | ... 123eetsdtdgdg43e4 |
870 | ... -----END PGP SIGNATURE----- |
871 | ... --BOUNDARY |
872 | @@ -2550,12 +2605,12 @@ |
873 | ... affects firefox |
874 | ... --BOUNDARY |
875 | ... Content-type: multipart/appledouble; boundary="SUBBOUNDARY" |
876 | - ... |
877 | + ... |
878 | ... --SUBBOUNDARY |
879 | ... Content-type: application/applefile |
880 | ... Content-disposition: attachment; filename="sampledata" |
881 | ... Content-tranfer-encoding: 7bit |
882 | - ... |
883 | + ... |
884 | ... qwert |
885 | ... --SUBBOUNDARY |
886 | ... Content-type: text/plain |
887 | @@ -2595,7 +2650,8 @@ |
888 | ... --BOUNDARY""" |
889 | >>> |
890 | >>> process_email(submit_mail) |
891 | - >>> new_message = getUtility(IMessageSet).get('comment-with-attachment')[0] |
892 | + >>> new_message = getUtility(IMessageSet).get( |
893 | + ... 'comment-with-attachment')[0] |
894 | >>> new_message in bug_one.messages |
895 | True |
896 | >>> print_attachments(new_message.bugattachments) |
897 | @@ -2741,7 +2797,7 @@ |
898 | ... Content-Transfer-Encoding: base64 |
899 | ... X-Attachment-Id: f_fcuhv1fz0 |
900 | ... Content-Disposition: attachment; filename=image.jpg |
901 | - ... |
902 | + ... |
903 | ... dGhpcyBpcyBub3QgYSByZWFsIEpQRyBmaWxl== |
904 | ... --BOUNDARY""" |
905 | >>> |
906 | @@ -2773,7 +2829,7 @@ |
907 | ... Content-Transfer-Encoding: base64 |
908 | ... X-Attachment-Id: f_fcuhv1fz0 |
909 | ... Content-Disposition: attachment |
910 | - ... |
911 | + ... |
912 | ... dGhpcyBpcyBub3QgYSByZWFsIEpQRyBmaWxl== |
913 | ... --BOUNDARY |
914 | ... Content-type: text/x-diff; name="sourcefile1.diff" |
915 | @@ -2799,7 +2855,7 @@ |
916 | >>> print_attachments(get_latest_added_bug().attachments) |
917 | LibraryFileAlias ... image/jpeg; name="image.jpg" UNSPECIFIED |
918 | this is not a real JPG file |
919 | - LibraryFileAlias sourcefile.diff text/x-diff; name="sourcefile1.diff" PATCH |
920 | + LibraryFileAlias ... text/x-diff; name="sourcefile1.diff" PATCH |
921 | this should be diff output. |
922 | |
923 | |
924 | @@ -2807,7 +2863,8 @@ |
925 | -- Bjorn Tillenius, 2005-05-20 |
926 | |
927 | |
928 | -== Reply to a comment on a remote bug == |
929 | +Reply to a comment on a remote bug |
930 | +---------------------------------- |
931 | |
932 | If someone uses the email interface to reply to a comment which was |
933 | imported into Launchpad from a remote bugtracker their reply will be |
Hi Benji,
Once again, these changes look good. I really like the use of named variables for time durations in ensure_ sane_signature_ timestamp( ). It really improves the readability.
One question I did have: did you have a pre-implementation call with the bugs team about this? IIRC at the last EPIC gmb was in the process of killing all doctests in bugs. I would think the bugs team members would have asked for unit tests of this new feature.
If it turns out that the bugs team does in fact want unit tests, then I am sure you can land this as-is and land a follow-up branch soon after.
Looks good, r=mars.
Maris