Skip to content

Commit 28bb674

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 e72fa4e commit 28bb674

File tree

2 files changed

+324
-86
lines changed

2 files changed

+324
-86
lines changed

gcloud/credentials.py

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@
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+
class _GAECreds(object):
41+
"""Dummy class if not in App Engine environment."""
42+
3243

3344
def get_credentials():
3445
"""Gets credentials implicitly from the current environment.
@@ -160,7 +171,66 @@ def _get_pem_key(credentials):
160171
return RSA.importKey(pem_text)
161172

162173

163-
def _get_signed_query_params(credentials, expiration, signature_string):
174+
def _get_signature_bytes(credentials, string_to_sign):
175+
"""Uses crypto attributes of credentials to sign a string/bytes.
176+
177+
:type credentials: :class:`client.SignedJwtAssertionCredentials`,
178+
:class:`service_account._ServiceAccountCredentials`,
179+
:class:`_GAECreds`
180+
:param credentials: The credentials used for signing text (typically
181+
involves the creation of an RSA key).
182+
183+
:type string_to_sign: string
184+
:param string_to_sign: The string to be signed by the credentials.
185+
186+
:rtype: bytes
187+
:returns: Signed bytes produced by the credentials.
188+
"""
189+
if isinstance(credentials, _GAECreds):
190+
_, signed_bytes = app_identity.sign_blob(string_to_sign)
191+
return signed_bytes
192+
else:
193+
pem_key = _get_pem_key(credentials)
194+
# Sign the string with the RSA key.
195+
signer = PKCS1_v1_5.new(pem_key)
196+
if not isinstance(string_to_sign, six.binary_type):
197+
string_to_sign = string_to_sign.encode('utf-8')
198+
signature_hash = SHA256.new(string_to_sign)
199+
return signer.sign(signature_hash)
200+
201+
202+
def _get_service_account_name(credentials):
203+
"""Determines service account name from a credentials object.
204+
205+
:type credentials: :class:`client.SignedJwtAssertionCredentials`,
206+
:class:`service_account._ServiceAccountCredentials`,
207+
:class:`_GAECreds`
208+
:param credentials: The credentials used to determine the service
209+
account name.
210+
211+
:type string_to_sign: string
212+
:param string_to_sign: The string to be signed by the credentials.
213+
214+
:rtype: bytes
215+
:returns: Signed bytes produced by the credentials.
216+
:raises: :class:`ValueError` if the credentials are not a valid service
217+
account type
218+
"""
219+
service_account_name = None
220+
if isinstance(credentials, client.SignedJwtAssertionCredentials):
221+
service_account_name = credentials.service_account_name
222+
elif isinstance(credentials, service_account._ServiceAccountCredentials):
223+
service_account_name = credentials._service_account_email
224+
elif _GAECreds is not None and isinstance(credentials, _GAECreds):
225+
service_account_name = app_identity.get_service_account_name()
226+
227+
if service_account_name is None:
228+
raise ValueError('Service account name could not be determined '
229+
'from credentials')
230+
return service_account_name
231+
232+
233+
def _get_signed_query_params(credentials, expiration, string_to_sign):
164234
"""Gets query parameters for creating a signed URL.
165235

166236
:type credentials: :class:`client.SignedJwtAssertionCredentials`,
@@ -171,27 +241,16 @@ def _get_signed_query_params(credentials, expiration, signature_string):
171241
:type expiration: int or long
172242
:param expiration: When the signed URL should expire.
173243

174-
:type signature_string: string
175-
:param signature_string: The string to be signed by the credentials.
244+
:type string_to_sign: string
245+
:param string_to_sign: The string to be signed by the credentials.
176246

177247
:rtype: dict
178248
:returns: Query parameters matching the signing credentials with a
179249
signed payload.
180250
"""
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)
251+
signature_bytes = _get_signature_bytes(credentials, string_to_sign)
188252
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.
253+
service_account_name = _get_service_account_name(credentials)
195254
return {
196255
'GoogleAccessId': service_account_name,
197256
'Expires': str(expiration),
@@ -277,7 +336,7 @@ def generate_signed_url(credentials, resource, expiration,
277336
expiration = _get_expiration_seconds(expiration)
278337

279338
# Generate the string to sign.
280-
signature_string = '\n'.join([
339+
string_to_sign = '\n'.join([
281340
method,
282341
content_md5 or '',
283342
content_type or '',
@@ -287,7 +346,7 @@ def generate_signed_url(credentials, resource, expiration,
287346
# Set the right query parameters.
288347
query_params = _get_signed_query_params(credentials,
289348
expiration,
290-
signature_string)
349+
string_to_sign)
291350

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

0 commit comments

Comments
 (0)