Skip to content

Commit 9f182cf

Browse files
committed
Adding ability to sign URL from GAE.
Also refactoring _get_signed_query_params and the related tests so that the signing process and service account name determination are isolated methods. Fixes #607.
1 parent 319963f commit 9f182cf

2 files changed

Lines changed: 323 additions & 86 deletions

File tree

gcloud/credentials.py

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@
2929
from oauth2client import service_account
3030
import pytz
3131

32+
try:
33+
from google.appengine.api import app_identity
34+
except ImportError:
35+
app_identity = None
36+
37+
try:
38+
from oauth2client.appengine import AppAssertionCredentials as _GAECreds
39+
except ImportError:
40+
_GAECreds = None
41+
3242

3343
def get_credentials():
3444
"""Gets credentials implicitly from the current environment.
@@ -160,7 +170,66 @@ def _get_pem_key(credentials):
160170
return RSA.importKey(pem_text)
161171

162172

163-
def _get_signed_query_params(credentials, expiration, signature_string):
173+
def _get_signature_bytes(credentials, string_to_sign):
174+
"""Uses crypto attributes of credentials to sign a string/bytes.
175+
176+
:type credentials: :class:`client.SignedJwtAssertionCredentials`,
177+
:class:`service_account._ServiceAccountCredentials`,
178+
:class:`_GAECreds`
179+
:param credentials: The credentials used for signing text (typically
180+
involves the creation of an RSA key).
181+
182+
:type string_to_sign: string
183+
:param string_to_sign: The string to be signed by the credentials.
184+
185+
:rtype: bytes
186+
:returns: Signed bytes produced by the credentials.
187+
"""
188+
if _GAECreds is not None and isinstance(credentials, _GAECreds):
189+
_, signed_bytes = app_identity.sign_blob(string_to_sign)
190+
return signed_bytes
191+
else:
192+
pem_key = _get_pem_key(credentials)
193+
# Sign the string with the RSA key.
194+
signer = PKCS1_v1_5.new(pem_key)
195+
if not isinstance(string_to_sign, six.binary_type):
196+
string_to_sign = string_to_sign.encode('utf-8')
197+
signature_hash = SHA256.new(string_to_sign)
198+
return signer.sign(signature_hash)
199+
200+
201+
def _get_service_account_name(credentials):
202+
"""Determines service account name from a credentials object.
203+
204+
:type credentials: :class:`client.SignedJwtAssertionCredentials`,
205+
:class:`service_account._ServiceAccountCredentials`,
206+
:class:`_GAECreds`
207+
:param credentials: The credentials used to determine the service
208+
account name.
209+
210+
:type string_to_sign: string
211+
:param string_to_sign: The string to be signed by the credentials.
212+
213+
:rtype: bytes
214+
:returns: Signed bytes produced by the credentials.
215+
:raises: :class:`ValueError` if the credentials are not a valid service
216+
account type
217+
"""
218+
service_account_name = None
219+
if isinstance(credentials, client.SignedJwtAssertionCredentials):
220+
service_account_name = credentials.service_account_name
221+
elif isinstance(credentials, service_account._ServiceAccountCredentials):
222+
service_account_name = credentials._service_account_email
223+
elif _GAECreds is not None and isinstance(credentials, _GAECreds):
224+
service_account_name = app_identity.get_service_account_name()
225+
226+
if service_account_name is None:
227+
raise ValueError('Service account name could not be determined '
228+
'from credentials')
229+
return service_account_name
230+
231+
232+
def _get_signed_query_params(credentials, expiration, string_to_sign):
164233
"""Gets query parameters for creating a signed URL.
165234
166235
:type credentials: :class:`client.SignedJwtAssertionCredentials`,
@@ -171,27 +240,16 @@ def _get_signed_query_params(credentials, expiration, signature_string):
171240
:type expiration: int or long
172241
:param expiration: When the signed URL should expire.
173242
174-
:type signature_string: string
175-
:param signature_string: The string to be signed by the credentials.
243+
:type string_to_sign: string
244+
:param string_to_sign: The string to be signed by the credentials.
176245
177246
:rtype: dict
178247
:returns: Query parameters matching the signing credentials with a
179248
signed payload.
180249
"""
181-
pem_key = _get_pem_key(credentials)
182-
# Sign the string with the RSA key.
183-
signer = PKCS1_v1_5.new(pem_key)
184-
if not isinstance(signature_string, six.binary_type):
185-
signature_string = signature_string.encode('utf-8')
186-
signature_hash = SHA256.new(signature_string)
187-
signature_bytes = signer.sign(signature_hash)
250+
signature_bytes = _get_signature_bytes(credentials, string_to_sign)
188251
signature = base64.b64encode(signature_bytes)
189-
190-
if isinstance(credentials, client.SignedJwtAssertionCredentials):
191-
service_account_name = credentials.service_account_name
192-
elif isinstance(credentials, service_account._ServiceAccountCredentials):
193-
service_account_name = credentials._service_account_email
194-
# We know one of the above must occur since `_get_pem_key` fails if not.
252+
service_account_name = _get_service_account_name(credentials)
195253
return {
196254
'GoogleAccessId': service_account_name,
197255
'Expires': str(expiration),
@@ -277,7 +335,7 @@ def generate_signed_url(credentials, resource, expiration,
277335
expiration = _get_expiration_seconds(expiration)
278336

279337
# Generate the string to sign.
280-
signature_string = '\n'.join([
338+
string_to_sign = '\n'.join([
281339
method,
282340
content_md5 or '',
283341
content_type or '',
@@ -287,7 +345,7 @@ def generate_signed_url(credentials, resource, expiration,
287345
# Set the right query parameters.
288346
query_params = _get_signed_query_params(credentials,
289347
expiration,
290-
signature_string)
348+
string_to_sign)
291349

292350
# Return the built URL.
293351
return '{endpoint}{resource}?{querystring}'.format(

0 commit comments

Comments
 (0)