Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(141)

Side by Side Diff: client/third_party/oauth2client/service_account.py

Issue 1768993002: Update oauth2client to v2.0.1 and googleapiclient to v1.5.0. Base URL: git@github.com:luci/luci-py.git@master
Patch Set: . Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2014 Google Inc. All rights reserved. 1 # Copyright 2014 Google Inc. All rights reserved.
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License. 4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at 5 # You may obtain a copy of the License at
6 # 6 #
7 # http://www.apache.org/licenses/LICENSE-2.0 7 # http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 # Unless required by applicable law or agreed to in writing, software 9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 12 # See the License for the specific language governing permissions and
13 # limitations under the License. 13 # limitations under the License.
14 14
15 """A service account credentials class. 15 """oauth2client Service account credentials class."""
16
17 This credentials class is implemented on top of rsa library.
18 """
19 16
20 import base64 17 import base64
18 import copy
19 import datetime
20 import json
21 import time 21 import time
22 22
23 from pyasn1.codec.ber import decoder
24 from pyasn1_modules.rfc5208 import PrivateKeyInfo
25 import rsa
26
27 from oauth2client import GOOGLE_REVOKE_URI 23 from oauth2client import GOOGLE_REVOKE_URI
28 from oauth2client import GOOGLE_TOKEN_URI 24 from oauth2client import GOOGLE_TOKEN_URI
29 from oauth2client._helpers import _json_encode 25 from oauth2client._helpers import _json_encode
30 from oauth2client._helpers import _to_bytes 26 from oauth2client._helpers import _from_bytes
31 from oauth2client._helpers import _urlsafe_b64encode 27 from oauth2client._helpers import _urlsafe_b64encode
32 from oauth2client import util 28 from oauth2client import util
33 from oauth2client.client import AssertionCredentials 29 from oauth2client.client import AssertionCredentials
34 30 from oauth2client.client import EXPIRY_FORMAT
35 31 from oauth2client.client import SERVICE_ACCOUNT
36 class _ServiceAccountCredentials(AssertionCredentials): 32 from oauth2client import crypt
37 """Class representing a service account (signed JWT) credential.""" 33
38 34
39 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 35 _PASSWORD_DEFAULT = 'notasecret'
40 36 _PKCS12_KEY = '_private_key_pkcs12'
41 def __init__(self, service_account_id, service_account_email, 37 _PKCS12_ERROR = r"""
42 private_key_id, private_key_pkcs8_text, scopes, 38 This library only implements PKCS#12 support via the pyOpenSSL library.
43 user_agent=None, token_uri=GOOGLE_TOKEN_URI, 39 Either install pyOpenSSL, or please convert the .p12 file
44 revoke_uri=GOOGLE_REVOKE_URI, **kwargs): 40 to .pem format:
45 41 $ cat key.p12 | \
46 super(_ServiceAccountCredentials, self).__init__( 42 > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
47 None, user_agent=user_agent, token_uri=token_uri, 43 > openssl rsa > key.pem
48 revoke_uri=revoke_uri) 44 """
49 45
50 self._service_account_id = service_account_id 46
47 class ServiceAccountCredentials(AssertionCredentials):
48 """Service Account credential for OAuth 2.0 signed JWT grants.
49
50 Supports
51
52 * JSON keyfile (typically contains a PKCS8 key stored as
53 PEM text)
54 * ``.p12`` key (stores PKCS12 key and certificate)
55
56 Makes an assertion to server using a signed JWT assertion in exchange
57 for an access token.
58
59 This credential does not require a flow to instantiate because it
60 represents a two legged flow, and therefore has all of the required
61 information to generate and refresh its own access tokens.
62
63 Args:
64 service_account_email: string, The email associated with the
65 service account.
66 signer: ``crypt.Signer``, A signer which can be used to sign content.
67 scopes: List or string, (Optional) Scopes to use when acquiring
68 an access token.
69 private_key_id: string, (Optional) Private key identifier. Typically
70 only used with a JSON keyfile. Can be sent in the
71 header of a JWT token assertion.
72 client_id: string, (Optional) Client ID for the project that owns the
73 service account.
74 user_agent: string, (Optional) User agent to use when sending
75 request.
76 kwargs: dict, Extra key-value pairs (both strings) to send in the
77 payload body when making an assertion.
78 """
79
80 MAX_TOKEN_LIFETIME_SECS = 3600
81 """Max lifetime of the token (one hour, in seconds)."""
82
83 NON_SERIALIZED_MEMBERS = (
84 frozenset(['_signer']) |
85 AssertionCredentials.NON_SERIALIZED_MEMBERS)
86 """Members that aren't serialized when object is converted to JSON."""
87
88 # Can be over-ridden by factory constructors. Used for
89 # serialization/deserialization purposes.
90 _private_key_pkcs8_pem = None
91 _private_key_pkcs12 = None
92 _private_key_password = None
93
94 def __init__(self,
95 service_account_email,
96 signer,
97 scopes='',
98 private_key_id=None,
99 client_id=None,
100 user_agent=None,
101 **kwargs):
102
103 super(ServiceAccountCredentials, self).__init__(
104 None, user_agent=user_agent)
105
51 self._service_account_email = service_account_email 106 self._service_account_email = service_account_email
107 self._signer = signer
108 self._scopes = util.scopes_to_string(scopes)
52 self._private_key_id = private_key_id 109 self._private_key_id = private_key_id
53 self._private_key = _get_private_key(private_key_pkcs8_text) 110 self.client_id = client_id
54 self._private_key_pkcs8_text = private_key_pkcs8_text
55 self._scopes = util.scopes_to_string(scopes)
56 self._user_agent = user_agent 111 self._user_agent = user_agent
57 self._token_uri = token_uri
58 self._revoke_uri = revoke_uri
59 self._kwargs = kwargs 112 self._kwargs = kwargs
60 113
114 def _to_json(self, strip, to_serialize=None):
115 """Utility function that creates JSON repr. of a credentials object.
116
117 Over-ride is needed since PKCS#12 keys will not in general be JSON
118 serializable.
119
120 Args:
121 strip: array, An array of names of members to exclude from the
122 JSON.
123 to_serialize: dict, (Optional) The properties for this object
124 that will be serialized. This allows callers to modify
125 before serializing.
126
127 Returns:
128 string, a JSON representation of this instance, suitable to pass to
129 from_json().
130 """
131 if to_serialize is None:
132 to_serialize = copy.copy(self.__dict__)
133 pkcs12_val = to_serialize.get(_PKCS12_KEY)
134 if pkcs12_val is not None:
135 to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val)
136 return super(ServiceAccountCredentials, self)._to_json(
137 strip, to_serialize=to_serialize)
138
139 @classmethod
140 def _from_parsed_json_keyfile(cls, keyfile_dict, scopes):
141 """Helper for factory constructors from JSON keyfile.
142
143 Args:
144 keyfile_dict: dict-like object, The parsed dictionary-like object
145 containing the contents of the JSON keyfile.
146 scopes: List or string, Scopes to use when acquiring an
147 access token.
148
149 Returns:
150 ServiceAccountCredentials, a credentials object created from
151 the keyfile contents.
152
153 Raises:
154 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
155 KeyError, if one of the expected keys is not present in
156 the keyfile.
157 """
158 creds_type = keyfile_dict.get('type')
159 if creds_type != SERVICE_ACCOUNT:
160 raise ValueError('Unexpected credentials type', creds_type,
161 'Expected', SERVICE_ACCOUNT)
162
163 service_account_email = keyfile_dict['client_email']
164 private_key_pkcs8_pem = keyfile_dict['private_key']
165 private_key_id = keyfile_dict['private_key_id']
166 client_id = keyfile_dict['client_id']
167
168 signer = crypt.Signer.from_string(private_key_pkcs8_pem)
169 credentials = cls(service_account_email, signer, scopes=scopes,
170 private_key_id=private_key_id,
171 client_id=client_id)
172 credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
173 return credentials
174
175 @classmethod
176 def from_json_keyfile_name(cls, filename, scopes=''):
177 """Factory constructor from JSON keyfile by name.
178
179 Args:
180 filename: string, The location of the keyfile.
181 scopes: List or string, (Optional) Scopes to use when acquiring an
182 access token.
183
184 Returns:
185 ServiceAccountCredentials, a credentials object created from
186 the keyfile.
187
188 Raises:
189 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
190 KeyError, if one of the expected keys is not present in
191 the keyfile.
192 """
193 with open(filename, 'r') as file_obj:
194 client_credentials = json.load(file_obj)
195 return cls._from_parsed_json_keyfile(client_credentials, scopes)
196
197 @classmethod
198 def from_json_keyfile_dict(cls, keyfile_dict, scopes=''):
199 """Factory constructor from parsed JSON keyfile.
200
201 Args:
202 keyfile_dict: dict-like object, The parsed dictionary-like object
203 containing the contents of the JSON keyfile.
204 scopes: List or string, (Optional) Scopes to use when acquiring an
205 access token.
206
207 Returns:
208 ServiceAccountCredentials, a credentials object created from
209 the keyfile.
210
211 Raises:
212 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`.
213 KeyError, if one of the expected keys is not present in
214 the keyfile.
215 """
216 return cls._from_parsed_json_keyfile(keyfile_dict, scopes)
217
218 @classmethod
219 def _from_p12_keyfile_contents(cls, service_account_email,
220 private_key_pkcs12,
221 private_key_password=None, scopes=''):
222 """Factory constructor from JSON keyfile.
223
224 Args:
225 service_account_email: string, The email associated with the
226 service account.
227 private_key_pkcs12: string, The contents of a PKCS#12 keyfile.
228 private_key_password: string, (Optional) Password for PKCS#12
229 private key. Defaults to ``notasecret``.
230 scopes: List or string, (Optional) Scopes to use when acquiring an
231 access token.
232
233 Returns:
234 ServiceAccountCredentials, a credentials object created from
235 the keyfile.
236
237 Raises:
238 NotImplementedError if pyOpenSSL is not installed / not the
239 active crypto library.
240 """
241 if private_key_password is None:
242 private_key_password = _PASSWORD_DEFAULT
243 if crypt.Signer is not crypt.OpenSSLSigner:
244 raise NotImplementedError(_PKCS12_ERROR)
245 signer = crypt.Signer.from_string(private_key_pkcs12,
246 private_key_password)
247 credentials = cls(service_account_email, signer, scopes=scopes)
248 credentials._private_key_pkcs12 = private_key_pkcs12
249 credentials._private_key_password = private_key_password
250 return credentials
251
252 @classmethod
253 def from_p12_keyfile(cls, service_account_email, filename,
254 private_key_password=None, scopes=''):
255 """Factory constructor from JSON keyfile.
256
257 Args:
258 service_account_email: string, The email associated with the
259 service account.
260 filename: string, The location of the PKCS#12 keyfile.
261 private_key_password: string, (Optional) Password for PKCS#12
262 private key. Defaults to ``notasecret``.
263 scopes: List or string, (Optional) Scopes to use when acquiring an
264 access token.
265
266 Returns:
267 ServiceAccountCredentials, a credentials object created from
268 the keyfile.
269
270 Raises:
271 NotImplementedError if pyOpenSSL is not installed / not the
272 active crypto library.
273 """
274 with open(filename, 'rb') as file_obj:
275 private_key_pkcs12 = file_obj.read()
276 return cls._from_p12_keyfile_contents(
277 service_account_email, private_key_pkcs12,
278 private_key_password=private_key_password, scopes=scopes)
279
280 @classmethod
281 def from_p12_keyfile_buffer(cls, service_account_email, file_buffer,
282 private_key_password=None, scopes=''):
283 """Factory constructor from JSON keyfile.
284
285 Args:
286 service_account_email: string, The email associated with the
287 service account.
288 file_buffer: stream, A buffer that implements ``read()``
289 and contains the PKCS#12 key contents.
290 private_key_password: string, (Optional) Password for PKCS#12
291 private key. Defaults to ``notasecret``.
292 scopes: List or string, (Optional) Scopes to use when acquiring an
293 access token.
294
295 Returns:
296 ServiceAccountCredentials, a credentials object created from
297 the keyfile.
298
299 Raises:
300 NotImplementedError if pyOpenSSL is not installed / not the
301 active crypto library.
302 """
303 private_key_pkcs12 = file_buffer.read()
304 return cls._from_p12_keyfile_contents(
305 service_account_email, private_key_pkcs12,
306 private_key_password=private_key_password, scopes=scopes)
307
61 def _generate_assertion(self): 308 def _generate_assertion(self):
62 """Generate the assertion that will be used in the request.""" 309 """Generate the assertion that will be used in the request."""
63
64 header = {
65 'alg': 'RS256',
66 'typ': 'JWT',
67 'kid': self._private_key_id
68 }
69
70 now = int(time.time()) 310 now = int(time.time())
71 payload = { 311 payload = {
72 'aud': self._token_uri, 312 'aud': self.token_uri,
73 'scope': self._scopes, 313 'scope': self._scopes,
74 'iat': now, 314 'iat': now,
75 'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS, 315 'exp': now + self.MAX_TOKEN_LIFETIME_SECS,
76 'iss': self._service_account_email 316 'iss': self._service_account_email,
77 } 317 }
78 payload.update(self._kwargs) 318 payload.update(self._kwargs)
79 319 return crypt.make_signed_jwt(self._signer, payload,
80 first_segment = _urlsafe_b64encode(_json_encode(header)) 320 key_id=self._private_key_id)
81 second_segment = _urlsafe_b64encode(_json_encode(payload))
82 assertion_input = first_segment + b'.' + second_segment
83
84 # Sign the assertion.
85 rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key,
86 'SHA-256')
87 signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=')
88
89 return assertion_input + b'.' + signature
90 321
91 def sign_blob(self, blob): 322 def sign_blob(self, blob):
92 # Ensure that it is bytes 323 """Cryptographically sign a blob (of bytes).
93 blob = _to_bytes(blob, encoding='utf-8') 324
94 return (self._private_key_id, 325 Implements abstract method
95 rsa.pkcs1.sign(blob, self._private_key, 'SHA-256')) 326 :meth:`oauth2client.client.AssertionCredentials.sign_blob`.
327
328 Args:
329 blob: bytes, Message to be signed.
330
331 Returns:
332 tuple, A pair of the private key ID used to sign the blob and
333 the signed contents.
334 """
335 return self._private_key_id, self._signer.sign(blob)
96 336
97 @property 337 @property
98 def service_account_email(self): 338 def service_account_email(self):
339 """Get the email for the current service account.
340
341 Returns:
342 string, The email associated with the service account.
343 """
99 return self._service_account_email 344 return self._service_account_email
100 345
101 @property 346 @property
102 def serialization_data(self): 347 def serialization_data(self):
348 # NOTE: This is only useful for JSON keyfile.
103 return { 349 return {
104 'type': 'service_account', 350 'type': 'service_account',
105 'client_id': self._service_account_id,
106 'client_email': self._service_account_email, 351 'client_email': self._service_account_email,
107 'private_key_id': self._private_key_id, 352 'private_key_id': self._private_key_id,
108 'private_key': self._private_key_pkcs8_text 353 'private_key': self._private_key_pkcs8_pem,
354 'client_id': self.client_id,
109 } 355 }
110 356
357 @classmethod
358 def from_json(cls, json_data):
359 """Deserialize a JSON-serialized instance.
360
361 Inverse to :meth:`to_json`.
362
363 Args:
364 json_data: dict or string, Serialized JSON (as a string or an
365 already parsed dictionary) representing a credential.
366
367 Returns:
368 ServiceAccountCredentials from the serialized data.
369 """
370 if not isinstance(json_data, dict):
371 json_data = json.loads(_from_bytes(json_data))
372
373 private_key_pkcs8_pem = None
374 pkcs12_val = json_data.get(_PKCS12_KEY)
375 password = None
376 if pkcs12_val is None:
377 private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem']
378 signer = crypt.Signer.from_string(private_key_pkcs8_pem)
379 else:
380 # NOTE: This assumes that private_key_pkcs8_pem is not also
381 # in the serialized data. This would be very incorrect
382 # state.
383 pkcs12_val = base64.b64decode(pkcs12_val)
384 password = json_data['_private_key_password']
385 signer = crypt.Signer.from_string(pkcs12_val, password)
386
387 credentials = cls(
388 json_data['_service_account_email'],
389 signer,
390 scopes=json_data['_scopes'],
391 private_key_id=json_data['_private_key_id'],
392 client_id=json_data['client_id'],
393 user_agent=json_data['_user_agent'],
394 **json_data['_kwargs']
395 )
396 if private_key_pkcs8_pem is not None:
397 credentials._private_key_pkcs8_pem = private_key_pkcs8_pem
398 if pkcs12_val is not None:
399 credentials._private_key_pkcs12 = pkcs12_val
400 if password is not None:
401 credentials._private_key_password = password
402 credentials.invalid = json_data['invalid']
403 credentials.access_token = json_data['access_token']
404 credentials.token_uri = json_data['token_uri']
405 credentials.revoke_uri = json_data['revoke_uri']
406 token_expiry = json_data.get('token_expiry', None)
407 if token_expiry is not None:
408 credentials.token_expiry = datetime.datetime.strptime(
409 token_expiry, EXPIRY_FORMAT)
410 return credentials
411
111 def create_scoped_required(self): 412 def create_scoped_required(self):
112 return not self._scopes 413 return not self._scopes
113 414
114 def create_scoped(self, scopes): 415 def create_scoped(self, scopes):
115 return _ServiceAccountCredentials(self._service_account_id, 416 result = self.__class__(self._service_account_email,
116 self._service_account_email, 417 self._signer,
117 self._private_key_id, 418 scopes=scopes,
118 self._private_key_pkcs8_text, 419 private_key_id=self._private_key_id,
119 scopes, 420 client_id=self.client_id,
120 user_agent=self._user_agent, 421 user_agent=self._user_agent,
121 token_uri=self._token_uri, 422 **self._kwargs)
122 revoke_uri=self._revoke_uri, 423 result.token_uri = self.token_uri
123 **self._kwargs) 424 result.revoke_uri = self.revoke_uri
124 425 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
125 426 result._private_key_pkcs12 = self._private_key_pkcs12
126 def _get_private_key(private_key_pkcs8_text): 427 result._private_key_password = self._private_key_password
127 """Get an RSA private key object from a pkcs8 representation.""" 428 return result
128 private_key_pkcs8_text = _to_bytes(private_key_pkcs8_text) 429
129 der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY') 430 def create_delegated(self, sub):
130 asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo()) 431 """Create credentials that act as domain-wide delegation of authority.
131 return rsa.PrivateKey.load_pkcs1( 432
132 asn1_private_key.getComponentByName('privateKey').asOctets(), 433 Use the ``sub`` parameter as the subject to delegate on behalf of
133 format='DER') 434 that user.
435
436 For example::
437
438 >>> account_sub = 'foo@email.com'
439 >>> delegate_creds = creds.create_delegated(account_sub)
440
441 Args:
442 sub: string, An email address that this service account will
443 act on behalf of (via domain-wide delegation).
444
445 Returns:
446 ServiceAccountCredentials, a copy of the current service account
447 updated to act on behalf of ``sub``.
448 """
449 new_kwargs = dict(self._kwargs)
450 new_kwargs['sub'] = sub
451 result = self.__class__(self._service_account_email,
452 self._signer,
453 scopes=self._scopes,
454 private_key_id=self._private_key_id,
455 client_id=self.client_id,
456 user_agent=self._user_agent,
457 **new_kwargs)
458 result.token_uri = self.token_uri
459 result.revoke_uri = self.revoke_uri
460 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem
461 result._private_key_pkcs12 = self._private_key_pkcs12
462 result._private_key_password = self._private_key_password
463 return result
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698