Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. | 1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """ | 5 """ |
| 6 Utilities for requesting information for a gerrit server via https. | 6 Utilities for requesting information for a gerrit server via https. |
| 7 | 7 |
| 8 https://gerrit-review.googlesource.com/Documentation/rest-api.html | 8 https://gerrit-review.googlesource.com/Documentation/rest-api.html |
| 9 """ | 9 """ |
| 10 | 10 |
| 11 import base64 | 11 import base64 |
| 12 import httplib | 12 import httplib |
| 13 import json | 13 import json |
| 14 import logging | 14 import logging |
| 15 import netrc | 15 import netrc |
| 16 import os | 16 import os |
| 17 import re | 17 import re |
| 18 import socket | |
| 18 import stat | 19 import stat |
| 19 import sys | 20 import sys |
| 20 import time | 21 import time |
| 21 import urllib | 22 import urllib |
| 23 import urlparse | |
| 22 from cStringIO import StringIO | 24 from cStringIO import StringIO |
| 23 | 25 |
| 24 _netrc_file = '_netrc' if sys.platform.startswith('win') else '.netrc' | |
| 25 _netrc_file = os.path.join(os.environ['HOME'], _netrc_file) | |
| 26 try: | |
| 27 NETRC = netrc.netrc(_netrc_file) | |
| 28 except IOError: | |
| 29 print >> sys.stderr, 'WARNING: Could not read netrc file %s' % _netrc_file | |
| 30 NETRC = netrc.netrc(os.devnull) | |
| 31 except netrc.NetrcParseError as e: | |
| 32 _netrc_stat = os.stat(e.filename) | |
| 33 if _netrc_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO): | |
| 34 print >> sys.stderr, ( | |
| 35 'WARNING: netrc file %s cannot be used because its file permissions ' | |
| 36 'are insecure. netrc file permissions should be 600.' % _netrc_file) | |
| 37 else: | |
| 38 print >> sys.stderr, ('ERROR: Cannot use netrc file %s due to a parsing ' | |
| 39 'error.' % _netrc_file) | |
| 40 raise | |
| 41 del _netrc_stat | |
| 42 NETRC = netrc.netrc(os.devnull) | |
| 43 del _netrc_file | |
| 44 | 26 |
| 45 LOGGER = logging.getLogger() | 27 LOGGER = logging.getLogger() |
| 46 TRY_LIMIT = 5 | 28 TRY_LIMIT = 5 |
| 47 | 29 |
| 30 | |
| 31 class BaseAuthenticator(object): | |
| 32 | |
| 33 def get_auth_header(self, host): | |
| 34 raise NotImplementedError() | |
| 35 | |
| 36 @staticmethod | |
| 37 def get(): | |
| 38 if GceAuthenticator.is_gce(): | |
| 39 return GceAuthenticator() | |
| 40 return NetrcAuthenticator() | |
| 41 | |
| 42 | |
| 43 class NetrcAuthenticator(BaseAuthenticator): | |
| 44 """BaseAuthenticator implementation that uses ".netrc" for token. | |
| 45 """ | |
| 46 | |
| 47 def __init__(self): | |
| 48 self.netrc = self._get_netrc() | |
| 49 | |
| 50 @staticmethod | |
| 51 def _get_netrc(): | |
| 52 path = '_netrc' if sys.platform.startswith('win') else '.netrc' | |
| 53 path = os.path.join(os.environ['HOME'], path) | |
| 54 try: | |
| 55 return netrc.netrc(path) | |
| 56 except IOError: | |
| 57 print >> sys.stderr, 'WARNING: Could not read netrc file %s' % path | |
| 58 return netrc.netrc(os.devnull) | |
| 59 except netrc.NetrcParseError as e: | |
| 60 st = os.stat(e.path) | |
| 61 if st.st_mode & (stat.S_IRWXG | stat.S_IRWXO): | |
| 62 print >> sys.stderr, ( | |
| 63 'WARNING: netrc file %s cannot be used because its file ' | |
| 64 'permissions are insecure. netrc file permissions should be ' | |
| 65 '600.' % path) | |
| 66 else: | |
| 67 print >> sys.stderr, ('ERROR: Cannot use netrc file %s due to a ' | |
| 68 'parsing error.' % path) | |
| 69 raise | |
| 70 return netrc.netrc(os.devnull) | |
| 71 | |
| 72 def get_auth_header(self, host): | |
| 73 auth = self.netrc.authenticators(host) | |
| 74 if auth: | |
| 75 return 'Basic %s' % (base64.b64encode('%s:%s' % (auth[0], auth[2]))) | |
| 76 return None | |
| 77 | |
| 78 | |
| 79 class GceAuthenticator(BaseAuthenticator): | |
| 80 """BaseAuthenticator implementation that uses GCE metadata service for token. | |
| 81 """ | |
| 82 | |
| 83 _INFO_URL = 'http://metadata.google.internal' | |
| 84 _ACQUIRE_URL = ('http://metadata/computeMetadata/v1/instance/' | |
| 85 'service-accounts/default/token') | |
| 86 _ACQUIRE_HEADERS = {"Metadata-Flavor": "Google"} | |
| 87 | |
| 88 _cache_is_gce = None | |
| 89 _token_cache = None | |
| 90 _token_expiration = None | |
| 91 | |
| 92 @classmethod | |
| 93 def is_gce(cls): | |
| 94 if cls._cache_is_gce is None: | |
| 95 cls._cache_is_gce = cls._test_is_gce() | |
| 96 return cls._cache_is_gce | |
| 97 | |
| 98 @classmethod | |
| 99 def _test_is_gce(cls): | |
| 100 # Based on https://cloud.google.com/compute/docs/metadata#runninggce | |
| 101 try: | |
| 102 resp = cls._get(cls._INFO_URL) | |
| 103 except socket.error: | |
| 104 # Could not resolve URL. | |
| 105 return False | |
| 106 return resp.getheader('Metadata-Flavor', None) == 'Google' | |
| 107 | |
| 108 @staticmethod | |
| 109 def _get(url, **kwargs): | |
| 110 p = urlparse.urlparse(url) | |
| 111 c = GetConnectionClass(protocol=p.scheme)(p.netloc) | |
| 112 c.request('GET', url, **kwargs) | |
| 113 return c.getresponse() | |
| 114 | |
| 115 @classmethod | |
| 116 def _get_token_dict(cls): | |
| 117 if cls._token_cache: | |
| 118 # If it expires within 25 seconds, refresh. | |
| 119 if cls._token_expiration > time.time() - 25: | |
| 120 return cls._token_cache | |
| 121 | |
| 122 resp = cls._get(cls._ACQUIRE_URL, headers=cls._ACQUIRE_HEADERS) | |
| 123 if resp.status != httplib.OK: | |
| 124 return None | |
| 125 cls._token_cache = token_dict = json.load(resp) | |
| 126 cls._token_expiration = token_dict['expires_in'] + time.time() | |
| 127 return cls._token_cache | |
| 128 | |
| 129 def get_auth_header(self, _host): | |
| 130 token_dict = self._get_token_dict() | |
| 131 if not token_dict: | |
| 132 return None | |
| 133 return '%(token_type)s %(access_token)s' % token_dict | |
|
smut
2015/09/18 22:32:43
Move classes down (at least below module-level con
dnj
2015/09/18 22:37:46
Done.
| |
| 134 | |
| 135 | |
| 48 # Controls the transport protocol used to communicate with gerrit. | 136 # Controls the transport protocol used to communicate with gerrit. |
| 49 # This is parameterized primarily to enable GerritTestCase. | 137 # This is parameterized primarily to enable GerritTestCase. |
| 50 GERRIT_PROTOCOL = 'https' | 138 GERRIT_PROTOCOL = 'https' |
| 51 | 139 |
| 52 | 140 |
| 53 class GerritError(Exception): | 141 class GerritError(Exception): |
| 54 """Exception class for errors commuicating with the gerrit-on-borg service.""" | 142 """Exception class for errors commuicating with the gerrit-on-borg service.""" |
| 55 def __init__(self, http_status, *args, **kwargs): | 143 def __init__(self, http_status, *args, **kwargs): |
| 56 super(GerritError, self).__init__(*args, **kwargs) | 144 super(GerritError, self).__init__(*args, **kwargs) |
| 57 self.http_status = http_status | 145 self.http_status = http_status |
| (...skipping 23 matching lines...) Expand all Loading... | |
| 81 return httplib.HTTPConnection | 169 return httplib.HTTPConnection |
| 82 else: | 170 else: |
| 83 raise RuntimeError( | 171 raise RuntimeError( |
| 84 "Don't know how to work with protocol '%s'" % protocol) | 172 "Don't know how to work with protocol '%s'" % protocol) |
| 85 | 173 |
| 86 | 174 |
| 87 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None): | 175 def CreateHttpConn(host, path, reqtype='GET', headers=None, body=None): |
| 88 """Opens an https connection to a gerrit service, and sends a request.""" | 176 """Opens an https connection to a gerrit service, and sends a request.""" |
| 89 headers = headers or {} | 177 headers = headers or {} |
| 90 bare_host = host.partition(':')[0] | 178 bare_host = host.partition(':')[0] |
| 91 auth = NETRC.authenticators(bare_host) | |
| 92 | 179 |
| 180 auth = BaseAuthenticator.get().get_auth_header(bare_host) | |
| 93 if auth: | 181 if auth: |
| 94 headers.setdefault('Authorization', 'Basic %s' % ( | 182 headers.setdefault('Authorization', auth) |
| 95 base64.b64encode('%s:%s' % (auth[0], auth[2])))) | |
| 96 else: | 183 else: |
| 97 LOGGER.debug('No authorization found in netrc for %s.' % bare_host) | 184 LOGGER.debug('No authorization found for %s.' % bare_host) |
| 98 | 185 |
| 99 if 'Authorization' in headers and not path.startswith('a/'): | 186 if 'Authorization' in headers and not path.startswith('a/'): |
| 100 url = '/a/%s' % path | 187 url = '/a/%s' % path |
| 101 else: | 188 else: |
| 102 url = '/%s' % path | 189 url = '/%s' % path |
| 103 | 190 |
| 104 if body: | 191 if body: |
| 105 body = json.JSONEncoder().encode(body) | 192 body = json.JSONEncoder().encode(body) |
| 106 headers.setdefault('Content-Type', 'application/json') | 193 headers.setdefault('Content-Type', 'application/json') |
| 107 if LOGGER.isEnabledFor(logging.DEBUG): | 194 if LOGGER.isEnabledFor(logging.DEBUG): |
| (...skipping 357 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 465 username = review.get('email', jmsg.get('name', '')) | 552 username = review.get('email', jmsg.get('name', '')) |
| 466 raise GerritError(200, 'Unable to set %s label for user "%s"' | 553 raise GerritError(200, 'Unable to set %s label for user "%s"' |
| 467 ' on change %s.' % (label, username, change)) | 554 ' on change %s.' % (label, username, change)) |
| 468 jmsg = GetChangeCurrentRevision(host, change) | 555 jmsg = GetChangeCurrentRevision(host, change) |
| 469 if not jmsg: | 556 if not jmsg: |
| 470 raise GerritError( | 557 raise GerritError( |
| 471 200, 'Could not get review information for change "%s"' % change) | 558 200, 'Could not get review information for change "%s"' % change) |
| 472 elif jmsg[0]['current_revision'] != revision: | 559 elif jmsg[0]['current_revision'] != revision: |
| 473 raise GerritError(200, 'While resetting labels on change "%s", ' | 560 raise GerritError(200, 'While resetting labels on change "%s", ' |
| 474 'a new patchset was uploaded.' % change) | 561 'a new patchset was uploaded.' % change) |
| OLD | NEW |