| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium 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 import base64 |
| 5 import collections | 6 import collections |
| 6 import copy | 7 import copy |
| 7 import json | 8 import json |
| 8 import logging | 9 import logging |
| 9 import os | 10 import os |
| 10 import re | 11 import re |
| 11 import socket | 12 import socket |
| 12 import sys | 13 import sys |
| 13 import time | 14 import time |
| 14 | 15 |
| 15 import httplib2 | 16 import httplib2 |
| 16 import oauth2client.client | 17 import oauth2client.client |
| 17 | 18 |
| 18 from googleapiclient import errors | 19 from googleapiclient import errors |
| 19 from infra_libs.ts_mon.common import http_metrics | 20 from infra_libs.ts_mon.common import http_metrics |
| 21 from oauth2client import util |
| 20 | 22 |
| 21 DEFAULT_SCOPES = ['email'] | 23 DEFAULT_SCOPES = ['email'] |
| 22 | 24 |
| 23 # default timeout for http requests, in seconds | 25 # default timeout for http requests, in seconds |
| 24 DEFAULT_TIMEOUT = 30 | 26 DEFAULT_TIMEOUT = 30 |
| 25 | 27 |
| 26 # This is part of the API. | 28 # This is part of the API. |
| 27 if sys.platform.startswith('win'): # pragma: no cover | 29 if sys.platform.startswith('win'): # pragma: no cover |
| 28 SERVICE_ACCOUNTS_CREDS_ROOT = 'C:\\creds\\service_accounts' | 30 SERVICE_ACCOUNTS_CREDS_ROOT = 'C:\\creds\\service_accounts' |
| 29 else: | 31 else: |
| (...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 155 credentials_filename, | 157 credentials_filename, |
| 156 scope=scope, | 158 scope=scope, |
| 157 service_accounts_creds_root=service_accounts_creds_root) | 159 service_accounts_creds_root=service_accounts_creds_root) |
| 158 | 160 |
| 159 if http_identifier: | 161 if http_identifier: |
| 160 http = InstrumentedHttp(http_identifier, timeout=timeout) | 162 http = InstrumentedHttp(http_identifier, timeout=timeout) |
| 161 else: | 163 else: |
| 162 http = httplib2.Http(timeout=timeout) | 164 http = httplib2.Http(timeout=timeout) |
| 163 return creds.authorize(http) | 165 return creds.authorize(http) |
| 164 | 166 |
| 167 |
| 168 class DelegateServiceAccountCredentials( |
| 169 oauth2client.client.AssertionCredentials): |
| 170 """Authorizes an HTTP client with a service account for which we are an actor. |
| 171 |
| 172 This class uses the IAM API to sign a JWT with the private key of another |
| 173 service account for which we have the "Service Account Actor" role. |
| 174 """ |
| 175 |
| 176 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds |
| 177 _SIGN_BLOB_URL = 'https://iam.googleapis.com/v1/%s:signBlob' |
| 178 |
| 179 def __init__(self, http, service_account_email, scopes, project='-'): |
| 180 """ |
| 181 Args: |
| 182 http: An httplib2.Http object that is authorized by another |
| 183 oauth2client.client.OAuth2Credentials with credentials that have the |
| 184 service account actor role on the service_account_email. |
| 185 service_account_email: The email address of the service account for which |
| 186 to obtain an access token. |
| 187 scopes: The desired scopes for the token. |
| 188 project: The cloud project to which service_account_email belongs. The |
| 189 default of '-' makes the IAM API figure it out for us. |
| 190 """ |
| 191 |
| 192 super(DelegateServiceAccountCredentials, self).__init__(None) |
| 193 self._service_account_email = service_account_email |
| 194 self._scopes = util.scopes_to_string(scopes) |
| 195 self._http = http |
| 196 self._name = 'projects/%s/serviceAccounts/%s' % ( |
| 197 project, service_account_email) |
| 198 |
| 199 def sign_blob(self, blob): |
| 200 response, content = self._http.request( |
| 201 self._SIGN_BLOB_URL % self._name, |
| 202 method='POST', |
| 203 body=json.dumps({'bytesToSign': base64.b64encode(blob)}), |
| 204 headers={'Content-Type': 'application/json'}) |
| 205 if response.status != 200: |
| 206 raise AuthError('Failed to sign blob as %s: %d %s' % ( |
| 207 self._service_account_email, response.status, response.reason)) |
| 208 |
| 209 data = json.loads(content) |
| 210 return data['keyId'], data['signature'] |
| 211 |
| 212 def _generate_assertion(self): |
| 213 # This is copied with small modifications from |
| 214 # oauth2client.service_account._ServiceAccountCredentials. |
| 215 |
| 216 header = { |
| 217 'alg': 'RS256', |
| 218 'typ': 'JWT', |
| 219 } |
| 220 |
| 221 now = int(time.time()) |
| 222 payload = { |
| 223 'aud': self.token_uri, |
| 224 'scope': self._scopes, |
| 225 'iat': now, |
| 226 'exp': now + self.MAX_TOKEN_LIFETIME_SECS, |
| 227 'iss': self._service_account_email, |
| 228 } |
| 229 |
| 230 assertion_input = ( |
| 231 self._urlsafe_b64encode(header) + b'.' + |
| 232 self._urlsafe_b64encode(payload)) |
| 233 |
| 234 # Sign the assertion. |
| 235 _, rsa_bytes = self.sign_blob(assertion_input) |
| 236 signature = rsa_bytes.rstrip(b'=') |
| 237 |
| 238 return assertion_input + b'.' + signature |
| 239 |
| 240 def _urlsafe_b64encode(self, data): |
| 241 # Copied verbatim from oauth2client.service_account. |
| 242 return base64.urlsafe_b64encode( |
| 243 json.dumps(data, separators=(',', ':')).encode('UTF-8')).rstrip(b'=') |
| 244 |
| 245 |
| 165 class RetriableHttp(object): | 246 class RetriableHttp(object): |
| 166 """A httplib2.Http object that retries on failure.""" | 247 """A httplib2.Http object that retries on failure.""" |
| 167 | 248 |
| 168 def __init__(self, http, max_tries=5, backoff_time=1, | 249 def __init__(self, http, max_tries=5, backoff_time=1, |
| 169 retrying_statuses_fn=None): | 250 retrying_statuses_fn=None): |
| 170 """ | 251 """ |
| 171 Args: | 252 Args: |
| 172 http: an httplib2.Http instance | 253 http: an httplib2.Http instance |
| 173 max_tries: a number of maximum tries | 254 max_tries: a number of maximum tries |
| 174 backoff_time: a number of seconds to sleep between retries | 255 backoff_time: a number of seconds to sleep between retries |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 209 def __getattr__(self, name): | 290 def __getattr__(self, name): |
| 210 return getattr(self._http, name) | 291 return getattr(self._http, name) |
| 211 | 292 |
| 212 def __setattr__(self, name, value): | 293 def __setattr__(self, name, value): |
| 213 if name in ('request', '_http', '_max_tries', '_backoff_time', | 294 if name in ('request', '_http', '_max_tries', '_backoff_time', |
| 214 '_retrying_statuses_fn'): | 295 '_retrying_statuses_fn'): |
| 215 self.__dict__[name] = value | 296 self.__dict__[name] = value |
| 216 else: | 297 else: |
| 217 setattr(self._http, name, value) | 298 setattr(self._http, name, value) |
| 218 | 299 |
| 300 |
| 219 class InstrumentedHttp(httplib2.Http): | 301 class InstrumentedHttp(httplib2.Http): |
| 220 """A httplib2.Http object that reports ts_mon metrics about its requests.""" | 302 """A httplib2.Http object that reports ts_mon metrics about its requests.""" |
| 221 | 303 |
| 222 def __init__(self, name, time_fn=time.time, timeout=DEFAULT_TIMEOUT, | 304 def __init__(self, name, time_fn=time.time, timeout=DEFAULT_TIMEOUT, |
| 223 **kwargs): | 305 **kwargs): |
| 224 """ | 306 """ |
| 225 Args: | 307 Args: |
| 226 name: An identifier for the HTTP requests made by this object. | 308 name: An identifier for the HTTP requests made by this object. |
| 227 time_fn: Function returning the current time in seconds. Use for testing | 309 time_fn: Function returning the current time in seconds. Use for testing |
| 228 purposes only. | 310 purposes only. |
| (...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 311 self.requests_made.append(self.HttpCall(uri, method, body, headers)) | 393 self.requests_made.append(self.HttpCall(uri, method, body, headers)) |
| 312 headers = None | 394 headers = None |
| 313 body = None | 395 body = None |
| 314 for candidate in self._uris: | 396 for candidate in self._uris: |
| 315 if candidate[0].match(uri): | 397 if candidate[0].match(uri): |
| 316 _, headers, body = candidate | 398 _, headers, body = candidate |
| 317 break | 399 break |
| 318 if not headers: | 400 if not headers: |
| 319 raise AssertionError("Unexpected request to %s" % uri) | 401 raise AssertionError("Unexpected request to %s" % uri) |
| 320 return httplib2.Response(headers), body | 402 return httplib2.Response(headers), body |
| OLD | NEW |