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 elif host_parts.scheme == 'http': | |
466 upload.logging.warning('Changing protocol to https') | |
467 self.host = 'https' + host[4:] | |
468 else: | |
469 msg = 'Invalid url provided: %s' % host | |
470 upload.logging.error(msg) | |
471 raise ValueError(msg) | |
472 | |
473 self.host = self.host.rstrip('/') | |
474 | |
475 self.extra_headers = extra_headers or {} | |
476 | |
477 creds = SignedJwtAssertionCredentials( | |
478 client_id, | |
479 client_private_key, | |
480 'https://www.googleapis.com/auth/userinfo.email', | |
481 private_key_password=private_key_password, | |
482 user_agent=user_agent) | |
483 | |
484 self._http = creds.authorize(httplib2.Http(timeout=timeout)) | |
485 | |
486 def Send(self, | |
487 request_path, | |
488 payload=None, | |
489 content_type='application/octet-stream', | |
490 timeout=None, | |
491 extra_headers=None, | |
492 **kwargs): | |
493 """Send a POST or GET request to the server. | |
494 | |
495 Args: | |
496 request_path: path on the server to hit. This is concatenated with the | |
497 value of 'host' provided to the constructor. | |
498 payload: request is a POST if not None, GET otherwise | |
499 timeout: in seconds | |
500 extra_headers: (dict) | |
501 """ | |
502 # This method signature should match upload.py:AbstractRpcServer.Send() | |
503 method = 'GET' | |
504 | |
505 headers = self.extra_headers.copy() | |
506 headers.update(extra_headers or {}) | |
507 | |
508 if payload is not None: | |
509 method = 'POST' | |
510 headers['Content-Type'] = content_type | |
511 raise NotImplementedError('POST requests are not yet supported.') | |
512 | |
513 prev_timeout = self._http.timeout | |
514 try: | |
515 if timeout: | |
516 self._http.timeout = timeout | |
517 # TODO(pgervais) implement some kind of retry mechanism (see upload.py). | |
518 url = self.host + request_path | |
519 if kwargs: | |
520 url += "?" + urllib.urlencode(kwargs) | |
521 | |
522 ret = self._http.request(url, | |
523 method=method, | |
524 body=payload, | |
525 headers=headers) | |
526 return ret[1] | |
527 | |
528 finally: | |
529 self._http.timeout = prev_timeout | |
530 | |
531 | |
532 class JwtOAuth2Rietveld(Rietveld): | |
533 """Access to Rietveld using OAuth authentication. | |
534 | |
535 This class is supposed to be used only by bots, since this kind of | |
536 access is restricted to service accounts. | |
537 """ | |
538 # The parent__init__ is not called on purpose. | |
M-A Ruel
2014/03/22 01:09:09
Not a fan. Either split up the code into a third b
pgervais
2014/03/24 20:27:36
Well, this class is kind of a hack anyway, because
| |
539 # pylint: disable=W0231 | |
540 def __init__(self, | |
541 url, | |
542 client_id, | |
543 client_private_key_file, | |
544 private_key_password=None, | |
545 extra_headers=None): | |
546 if private_key_password is None: # '' means 'empty password' | |
547 private_key_password = 'notasecret' | |
548 | |
549 self.url = url.rstrip('/') | |
550 with open(client_private_key_file, 'rb') as f: | |
551 client_private_key = f.read() | |
552 self.rpc_server = OAuthRpcServer(url, | |
553 client_id, | |
554 client_private_key, | |
555 private_key_password=private_key_password, | |
556 extra_headers=extra_headers or {}) | |
557 self._xsrf_token = None | |
558 self._xsrf_token_time = None | |
559 | |
560 | |
441 class CachingRietveld(Rietveld): | 561 class CachingRietveld(Rietveld): |
442 """Caches the common queries. | 562 """Caches the common queries. |
443 | 563 |
444 Not to be used in long-standing processes, like the commit queue. | 564 Not to be used in long-standing processes, like the commit queue. |
445 """ | 565 """ |
446 def __init__(self, *args, **kwargs): | 566 def __init__(self, *args, **kwargs): |
447 super(CachingRietveld, self).__init__(*args, **kwargs) | 567 super(CachingRietveld, self).__init__(*args, **kwargs) |
448 self._cache = {} | 568 self._cache = {} |
449 | 569 |
450 def _lookup(self, function_name, args, update): | 570 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 | 679 def trigger_try_jobs( # pylint:disable=R0201 |
560 self, issue, patchset, reason, clobber, revision, builders_and_tests, | 680 self, issue, patchset, reason, clobber, revision, builders_and_tests, |
561 master=None): | 681 master=None): |
562 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 682 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
563 (builders_and_tests, issue)) | 683 (builders_and_tests, issue)) |
564 | 684 |
565 def trigger_distributed_try_jobs( # pylint:disable=R0201 | 685 def trigger_distributed_try_jobs( # pylint:disable=R0201 |
566 self, issue, patchset, reason, clobber, revision, masters): | 686 self, issue, patchset, reason, clobber, revision, masters): |
567 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % | 687 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % |
568 (masters, issue)) | 688 (masters, issue)) |
OLD | NEW |