Chromium Code Reviews| 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 |