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 |
| 25 import urlparse | |
| 26 | |
| 27 import patch | |
| 24 | 28 |
| 25 from third_party import upload | 29 from third_party import upload |
| 26 import patch | 30 from third_party.oauth2client.client import SignedJwtAssertionCredentials |
| 31 from third_party import httplib2 | |
| 27 | 32 |
| 28 # Hack out upload logging.info() | 33 # Hack out upload logging.info() |
| 29 upload.logging = logging.getLogger('upload') | 34 upload.logging = logging.getLogger('upload') |
| 30 # Mac pylint choke on this line. | 35 # Mac pylint choke on this line. |
| 31 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 | 36 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 |
| 32 | 37 |
| 33 | 38 |
| 34 class Rietveld(object): | 39 class Rietveld(object): |
| 35 """Accesses rietveld.""" | 40 """Accesses rietveld.""" |
| 36 def __init__(self, url, email, password, extra_headers=None): | 41 def __init__(self, url, email, password, extra_headers=None): |
| 37 self.url = url.rstrip('/') | 42 self.url = url.rstrip('/') |
| 38 # TODO(maruel): It's not awesome but maybe necessary to retrieve the value. | 43 # 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 | 44 # 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 | 45 # needed to be recreated from the credentials. Instead, it should pass the |
| 41 # email and the cookie. | 46 # email and the cookie. |
| 42 self.email = email | |
| 43 self.password = password | |
| 44 if email and password: | 47 if email and password: |
| 45 get_creds = lambda: (email, password) | 48 get_creds = lambda: (email, password) |
| 46 self.rpc_server = upload.HttpRpcServer( | 49 self.rpc_server = upload.HttpRpcServer( |
| 47 self.url, | 50 self.url, |
| 48 get_creds, | 51 get_creds, |
| 49 extra_headers=extra_headers or {}) | 52 extra_headers=extra_headers or {}) |
| 50 else: | 53 else: |
| 51 if email == '': | 54 if email == '': |
| 52 # If email is given as an empty string, then assume we want to make | 55 # If email is given as an empty string, then assume we want to make |
| 53 # requests that do not need authentication. Bypass authentication by | 56 # requests that do not need authentication. Bypass authentication by |
| (...skipping 377 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 431 raise | 434 raise |
| 432 # If reaching this line, loop again. Uses a small backoff. | 435 # If reaching this line, loop again. Uses a small backoff. |
| 433 time.sleep(1+maxtries*2) | 436 time.sleep(1+maxtries*2) |
| 434 finally: | 437 finally: |
| 435 upload.ErrorExit = old_error_exit | 438 upload.ErrorExit = old_error_exit |
| 436 | 439 |
| 437 # DEPRECATED. | 440 # DEPRECATED. |
| 438 Send = get | 441 Send = get |
| 439 | 442 |
| 440 | 443 |
| 444 class OAuthRpcServer(object): | |
| 445 def __init__(self, | |
| 446 host, | |
| 447 client_id, | |
| 448 client_private_key, | |
| 449 private_key_password='notasecret', | |
| 450 user_agent=None, | |
| 451 timeout=None, | |
| 452 extra_headers=None): | |
| 453 """Wrapper around httplib2.Http() that handles authentication. | |
| 454 | |
| 455 client_id: client id for service account | |
| 456 client_private_key: encrypted private key, as a string | |
| 457 private_key_password: password used to decrypt the private key | |
| 458 """ | |
| 459 | |
| 460 # Enforce https | |
| 461 host_parts = urlparse.urlparse(host) | |
| 462 | |
| 463 if host_parts.scheme == 'https': # fine | |
| 464 self.host = host | |
| 465 | |
| 466 elif host_parts.scheme == 'http': | |
| 467 upload.logging.warning('Changing protocol to https') | |
| 468 self.host = 'https' + host[4:] | |
| 469 | |
|
M-A Ruel
2014/03/15 01:15:31
optional nit: I would not put these empty lines, i
pgervais
2014/03/17 17:35:11
Done.
| |
| 470 else: | |
| 471 msg = 'Invalid url provided: %s' % host | |
| 472 upload.logging.error(msg) | |
| 473 raise ValueError(msg) | |
| 474 | |
| 475 self.host = self.host.rstrip('/') | |
| 476 | |
| 477 self.extra_headers = extra_headers or {} | |
| 478 creds = SignedJwtAssertionCredentials( | |
| 479 client_id, | |
| 480 client_private_key, | |
| 481 'https://www.googleapis.com/auth/userinfo.email', | |
| 482 private_key_password, | |
| 483 user_agent=user_agent) | |
| 484 | |
| 485 http = httplib2.Http(timeout=timeout) | |
| 486 self._http = creds.authorize(http) | |
|
M-A Ruel
2014/03/15 01:15:31
self._http = creds.authorize(httplib2.Http(timeout
pgervais
2014/03/17 17:35:11
Done.
| |
| 487 | |
| 488 def Send(self, | |
| 489 request_path, | |
| 490 payload=None, | |
| 491 content_type='application/octet-stream', | |
| 492 timeout=None, | |
| 493 extra_headers=None, | |
| 494 **kwargs): | |
| 495 """Send a POST or GET request to the server. | |
| 496 | |
| 497 Args: | |
| 498 request_path: path on the server to hit. This is concatenated with the | |
| 499 value of 'host' provided to the constructor. | |
| 500 payload: request is a POST if not None, GET otherwise | |
| 501 timeout: in seconds | |
| 502 extra_headers: (dict) | |
| 503 """ | |
| 504 # This method signature should match upload.py:AbstractRpcServer.Send() | |
| 505 method = 'GET' | |
| 506 | |
| 507 headers = self.extra_headers.copy() | |
| 508 headers.update(extra_headers or {}) | |
| 509 | |
| 510 if payload is not None: | |
| 511 method = 'POST' | |
| 512 headers['Content-Type'] = content_type | |
| 513 raise NotImplementedError('POST requests are not yet supported.') | |
| 514 | |
| 515 prev_timeout = self._http.timeout | |
| 516 try: | |
| 517 if timeout: | |
| 518 self._http.timeout = timeout | |
| 519 # TODO(pgervais) implement some kind of retry mechanism (see upload.py). | |
| 520 url = self.host + request_path | |
| 521 if kwargs: | |
| 522 url += "?" + urllib.urlencode(kwargs) | |
| 523 | |
| 524 ret = self._http.request(url, | |
| 525 method=method, | |
| 526 body=payload, | |
| 527 headers=headers) | |
| 528 return ret[1] | |
| 529 | |
| 530 finally: | |
| 531 self._http.timeout = prev_timeout | |
| 532 | |
| 533 | |
| 534 class JwtOAuth2Rietveld(Rietveld): | |
| 535 """Access to Rietveld using OAuth authentication. | |
| 536 | |
| 537 This class is supposed to be used only by bots, since this kind of | |
| 538 access is restricted to service accounts. | |
| 539 """ | |
| 540 # The parent__init__ is not called on purpose. | |
| 541 # pylint: disable=W0231 | |
| 542 def __init__(self, | |
| 543 url, | |
| 544 client_id, | |
| 545 client_private_key_file, | |
| 546 private_key_password=None, | |
| 547 extra_headers=None): | |
| 548 if private_key_password is None: # '' means 'empty password' | |
| 549 private_key_password = 'notasecret' | |
| 550 | |
| 551 self.url = url.rstrip('/') | |
| 552 with open(client_private_key_file, 'rb') as f: | |
| 553 client_private_key = f.read() | |
| 554 self.rpc_server = OAuthRpcServer(url, | |
| 555 client_id, | |
| 556 client_private_key, | |
| 557 private_key_password=private_key_password, | |
| 558 extra_headers=extra_headers or {}) | |
| 559 self._xsrf_token = None | |
| 560 self._xsrf_token_time = None | |
| 561 | |
| 562 | |
| 441 class CachingRietveld(Rietveld): | 563 class CachingRietveld(Rietveld): |
| 442 """Caches the common queries. | 564 """Caches the common queries. |
| 443 | 565 |
| 444 Not to be used in long-standing processes, like the commit queue. | 566 Not to be used in long-standing processes, like the commit queue. |
| 445 """ | 567 """ |
| 446 def __init__(self, *args, **kwargs): | 568 def __init__(self, *args, **kwargs): |
| 447 super(CachingRietveld, self).__init__(*args, **kwargs) | 569 super(CachingRietveld, self).__init__(*args, **kwargs) |
| 448 self._cache = {} | 570 self._cache = {} |
| 449 | 571 |
| 450 def _lookup(self, function_name, args, update): | 572 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 | 681 def trigger_try_jobs( # pylint:disable=R0201 |
| 560 self, issue, patchset, reason, clobber, revision, builders_and_tests, | 682 self, issue, patchset, reason, clobber, revision, builders_and_tests, |
| 561 master=None): | 683 master=None): |
| 562 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 684 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
| 563 (builders_and_tests, issue)) | 685 (builders_and_tests, issue)) |
| 564 | 686 |
| 565 def trigger_distributed_try_jobs( # pylint:disable=R0201 | 687 def trigger_distributed_try_jobs( # pylint:disable=R0201 |
| 566 self, issue, patchset, reason, clobber, revision, masters): | 688 self, issue, patchset, reason, clobber, revision, masters): |
| 567 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 689 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
| 568 (masters, issue)) | 690 (masters, issue)) |
| OLD | NEW |