OLD | NEW |
---|---|
1 # coding: utf-8 | 1 # coding: utf-8 |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 """Defines class Rietveld to easily access a rietveld instance. | 5 """Defines class Rietveld to easily access a rietveld instance. |
6 | 6 |
7 Security implications: | 7 Security implications: |
8 | 8 |
9 The following hypothesis are made: | 9 The following hypothesis are made: |
10 - Rietveld enforces: | 10 - Rietveld enforces: |
11 - Nobody else than issue owner can upload a patch set | 11 - Nobody else than issue owner can upload a patch set |
12 - Verifies the issue owner credentials when creating new issues | 12 - Verifies the issue owner credentials when creating new issues |
13 - A issue owner can't change once the issue is created | 13 - A issue owner can't change once the issue is created |
14 - A patch set cannot be modified | 14 - A patch set cannot be modified |
15 """ | 15 """ |
16 | 16 |
17 import copy | 17 import copy |
18 import json | 18 import json |
19 import logging | 19 import logging |
20 import re | 20 import re |
21 import ssl | 21 import ssl |
22 import time | 22 import time |
23 import urllib | |
23 import urllib2 | 24 import urllib2 |
24 | 25 |
26 import patch | |
27 | |
25 from third_party import upload | 28 from third_party import upload |
26 import patch | 29 from third_party.oauth2client.client import SignedJwtAssertionCredentials |
30 from third_party import httplib2 | |
27 | 31 |
28 # Hack out upload logging.info() | 32 # Hack out upload logging.info() |
29 upload.logging = logging.getLogger('upload') | 33 upload.logging = logging.getLogger('upload') |
30 # Mac pylint choke on this line. | 34 # Mac pylint choke on this line. |
31 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 | 35 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 |
32 | 36 |
33 | 37 |
34 class Rietveld(object): | 38 class Rietveld(object): |
35 """Accesses rietveld.""" | 39 """Accesses rietveld.""" |
36 def __init__(self, url, email, password, extra_headers=None): | 40 def __init__(self, url, email, password, extra_headers=None): |
37 self.url = url.rstrip('/') | 41 self.url = url.rstrip('/') |
38 # TODO(maruel): It's not awesome but maybe necessary to retrieve the value. | 42 # TODO(maruel): It's not awesome but maybe necessary to retrieve the value. |
39 # It happens when the presubmit check is ran out of process, the cookie | 43 # It happens when the presubmit check is ran out of process, the cookie |
40 # needed to be recreated from the credentials. Instead, it should pass the | 44 # needed to be recreated from the credentials. Instead, it should pass the |
41 # email and the cookie. | 45 # email and the cookie. |
42 self.email = email | |
43 self.password = password | |
44 if email and password: | 46 if email and password: |
45 get_creds = lambda: (email, password) | 47 get_creds = lambda: (email, password) |
46 self.rpc_server = upload.HttpRpcServer( | 48 self.rpc_server = upload.HttpRpcServer( |
47 self.url, | 49 self.url, |
48 get_creds, | 50 get_creds, |
49 extra_headers=extra_headers or {}) | 51 extra_headers=extra_headers or {}) |
50 else: | 52 else: |
51 if email == '': | 53 if email == '': |
52 # If email is given as an empty string, then assume we want to make | 54 # If email is given as an empty string, then assume we want to make |
53 # requests that do not need authentication. Bypass authentication by | 55 # requests that do not need authentication. Bypass authentication by |
(...skipping 377 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
431 raise | 433 raise |
432 # If reaching this line, loop again. Uses a small backoff. | 434 # If reaching this line, loop again. Uses a small backoff. |
433 time.sleep(1+maxtries*2) | 435 time.sleep(1+maxtries*2) |
434 finally: | 436 finally: |
435 upload.ErrorExit = old_error_exit | 437 upload.ErrorExit = old_error_exit |
436 | 438 |
437 # DEPRECATED. | 439 # DEPRECATED. |
438 Send = get | 440 Send = get |
439 | 441 |
440 | 442 |
443 class OAuthRpcServer(): | |
M-A Ruel
2014/03/13 01:31:27
(object)
pgervais
2014/03/13 22:38:38
Good catch, thanks.
| |
444 def __init__(self, | |
445 host, | |
446 client_id, | |
447 client_private_key, | |
448 private_key_password='notasecret', | |
449 user_agent=None, | |
450 timeout=None, | |
451 extra_headers=None): | |
452 """Wrapper around httplib2.Http() that handles authentication. | |
453 | |
454 client_id: client id for service account | |
455 client_private_key: encrypted private key, as a string | |
456 private_key_password: password used to decrypt the private key | |
457 """ | |
458 | |
459 # Enforce https | |
460 host_parts = host.split('://') | |
M-A Ruel
2014/03/13 01:31:27
that's unorthodox.
Vadim Sh.
2014/03/13 18:28:42
Yeah.. Use urlparse module.
pgervais
2014/03/13 22:38:38
That's better indeed. But calling split() does exa
| |
461 if host_parts[0] == 'https': # fine | |
462 self.host = host | |
463 | |
464 elif len(host_parts) == 1: # no protocol specified | |
465 self.host = 'https://' + host | |
466 | |
467 elif len(host_parts) == 2 and host_parts[0] == 'http': | |
468 upload.logging.warning('Changing protocol to https') | |
469 self.host = 'https://' + host_parts[1] | |
470 | |
471 else: | |
472 msg = 'Invalid url provided: %s' % host | |
473 upload.logging.error(msg) | |
474 raise ValueError(msg) | |
475 | |
476 if self.host.endswith('/'): | |
Vadim Sh.
2014/03/13 18:28:42
self.host.rstrip('/')
pgervais
2014/03/13 22:38:38
Done.
| |
477 self.host = self.host[:-1] | |
478 | |
479 self.extra_headers = extra_headers or {} | |
480 creds = SignedJwtAssertionCredentials( | |
481 client_id, | |
482 client_private_key, | |
483 'https://www.googleapis.com/auth/userinfo.email', | |
484 private_key_password, | |
485 user_agent=user_agent) | |
486 | |
487 http = httplib2.Http(timeout=timeout) | |
488 self._http = creds.authorize(http) | |
Vadim Sh.
2014/03/13 18:28:42
It makes the network call here, right? How apply_i
pgervais
2014/03/13 22:38:38
This only decorates the http object, no network ac
| |
489 | |
M-A Ruel
2014/03/13 01:31:27
remove one line
pgervais
2014/03/13 22:38:38
Done.
| |
490 | |
491 def Send(self, | |
492 request_path, | |
493 payload=None, | |
494 content_type="application/octet-stream", | |
M-A Ruel
2014/03/13 01:31:27
use single quote throughout.
pgervais
2014/03/13 22:38:38
I don't know why, but I find it horribly difficult
| |
495 timeout=None, | |
496 extra_headers=None, | |
497 **kwargs): | |
498 """Send a POST or GET request to the server. | |
499 | |
500 Args: | |
501 request_path: path on the server to hit. This is concatenated with the | |
M-A Ruel
2014/03/13 01:31:27
Align +2
pgervais
2014/03/13 22:38:38
Done.
| |
502 value of 'host' provided to the constructor. | |
503 payload: request is a POST if not None, GET otherwise | |
504 timeout: in seconds | |
505 extra_headers: (dict) | |
506 """ | |
507 # This method signature should match upload.py:AbstractRpcServer.Send() | |
508 method = 'GET' | |
509 | |
510 if extra_headers: | |
511 headers = self.extra_headers.copy() | |
512 headers.update(headers) | |
M-A Ruel
2014/03/13 01:31:27
ugh?
pgervais
2014/03/13 22:38:38
It took me some time to recall what I meant here :
| |
513 else: | |
514 headers = self.extra_headers | |
M-A Ruel
2014/03/13 01:31:27
I'd do the copy unconditionally for simplicity; it
pgervais
2014/03/13 22:38:38
Done.
| |
515 | |
516 if payload is not None: | |
517 method = 'POST' | |
518 headers['Content-Type'] = content_type | |
519 raise NotImplementedError('POST requests are not yet supported.') | |
520 | |
M-A Ruel
2014/03/13 01:31:27
never 2 empty lines inside a function
pgervais
2014/03/13 22:38:38
Done.
| |
521 | |
522 prev_timeout = self._http.timeout | |
523 try: | |
524 if timeout: | |
525 self._http.timeout = timeout | |
M-A Ruel
2014/03/13 01:31:27
That's a tad surprising.
| |
526 # TODO(pgervais) implement some kind of retry mechanism (see upload.py). | |
527 url = self.host + request_path | |
528 print('Trying URL: ' + url) | |
M-A Ruel
2014/03/13 01:31:27
Remove.
| |
529 args = dict(kwargs) | |
M-A Ruel
2014/03/13 01:31:27
It's a dict already.
pgervais
2014/03/13 22:38:38
I copy-pasted part of this code from upload.py, an
| |
530 if args: | |
531 url += "?" + urllib.urlencode(args) | |
532 | |
533 ret = self._http.request(url, | |
534 method=method, | |
535 body=payload, | |
536 headers=headers) | |
537 return ret[1] | |
538 | |
539 finally: | |
540 self._http.timeout = prev_timeout | |
M-A Ruel
2014/03/13 01:31:27
Not a fan.
pgervais
2014/03/13 22:38:38
I have no strong opinion on that matter. I'm fine
| |
541 | |
542 | |
543 class OAuthRietveld(Rietveld): | |
M-A Ruel
2014/03/13 01:31:27
But it's really JwtOAuth2Rietveld. Vadim can confi
Vadim Sh.
2014/03/13 18:28:42
I don't have preferred semantic, but I agree that
pgervais
2014/03/13 22:38:38
Done.
| |
544 """Access to Rietveld using OAuth authentication. | |
545 | |
546 This class is supposed to be used only by bots, since this kind of | |
547 access is restricted to service accounts. | |
548 """ | |
549 # The parent__init__ is not called on purpose. | |
550 def __init__(self, # pylint: disable=W0231 | |
Vadim Sh.
2014/03/13 18:28:42
Put pylint disable after ':'.
pgervais
2014/03/13 22:38:38
Done.
| |
551 url, | |
552 client_id, | |
553 client_private_key_file, | |
554 private_key_password='notasecret', | |
555 extra_headers=None): | |
556 self.url = url.rstrip('/') | |
557 with open(client_private_key_file, 'rb') as f: | |
558 client_private_key = f.read() | |
559 self.rpc_server = OAuthRpcServer(url, | |
560 client_id, | |
561 client_private_key, | |
562 private_key_password=private_key_password, | |
563 extra_headers=extra_headers or {}) | |
564 self._xsrf_token = None | |
565 self._xsrf_token_time = None | |
566 | |
567 | |
441 class CachingRietveld(Rietveld): | 568 class CachingRietveld(Rietveld): |
442 """Caches the common queries. | 569 """Caches the common queries. |
443 | 570 |
444 Not to be used in long-standing processes, like the commit queue. | 571 Not to be used in long-standing processes, like the commit queue. |
445 """ | 572 """ |
446 def __init__(self, *args, **kwargs): | 573 def __init__(self, *args, **kwargs): |
447 super(CachingRietveld, self).__init__(*args, **kwargs) | 574 super(CachingRietveld, self).__init__(*args, **kwargs) |
448 self._cache = {} | 575 self._cache = {} |
449 | 576 |
450 def _lookup(self, function_name, args, update): | 577 def _lookup(self, function_name, args, update): |
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
559 def trigger_try_jobs( # pylint:disable=R0201 | 686 def trigger_try_jobs( # pylint:disable=R0201 |
560 self, issue, patchset, reason, clobber, revision, builders_and_tests, | 687 self, issue, patchset, reason, clobber, revision, builders_and_tests, |
561 master=None): | 688 master=None): |
562 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 689 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
563 (builders_and_tests, issue)) | 690 (builders_and_tests, issue)) |
564 | 691 |
565 def trigger_distributed_try_jobs( # pylint:disable=R0201 | 692 def trigger_distributed_try_jobs( # pylint:disable=R0201 |
566 self, issue, patchset, reason, clobber, revision, masters): | 693 self, issue, patchset, reason, clobber, revision, masters): |
567 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 694 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
568 (masters, issue)) | 695 (masters, issue)) |
OLD | NEW |