Skip to content

Commit abc8061

Browse files
authored
feat: IAM signBlob retry and universe domain support (#1380)
* feat: IAM signBlob retries * support universe domain and update tests * update test credentials * use ud signing bucket fixture
1 parent 0cfddf4 commit abc8061

File tree

5 files changed

+92
-8
lines changed

5 files changed

+92
-8
lines changed

google/cloud/storage/_signing.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
from google.auth import exceptions
2929
from google.auth.transport import requests
3030
from google.cloud import _helpers
31+
from google.cloud.storage._helpers import _DEFAULT_UNIVERSE_DOMAIN
3132
from google.cloud.storage._helpers import _NOW
3233
from google.cloud.storage._helpers import _UTC
34+
from google.cloud.storage.retry import DEFAULT_RETRY
3335

3436

3537
# `google.cloud.storage._signing.NOW` is deprecated.
@@ -271,6 +273,7 @@ def generate_signed_url_v2(
271273
query_parameters=None,
272274
service_account_email=None,
273275
access_token=None,
276+
universe_domain=None,
274277
):
275278
"""Generate a V2 signed URL to provide query-string auth'n to a resource.
276279
@@ -384,7 +387,9 @@ def generate_signed_url_v2(
384387
# See https://github.com/googleapis/google-cloud-python/issues/922
385388
# Set the right query parameters.
386389
if access_token and service_account_email:
387-
signature = _sign_message(string_to_sign, access_token, service_account_email)
390+
signature = _sign_message(
391+
string_to_sign, access_token, service_account_email, universe_domain
392+
)
388393
signed_query_params = {
389394
"GoogleAccessId": service_account_email,
390395
"Expires": expiration_stamp,
@@ -432,6 +437,7 @@ def generate_signed_url_v4(
432437
query_parameters=None,
433438
service_account_email=None,
434439
access_token=None,
440+
universe_domain=None,
435441
_request_timestamp=None, # for testing only
436442
):
437443
"""Generate a V4 signed URL to provide query-string auth'n to a resource.
@@ -623,7 +629,9 @@ def generate_signed_url_v4(
623629
string_to_sign = "\n".join(string_elements)
624630

625631
if access_token and service_account_email:
626-
signature = _sign_message(string_to_sign, access_token, service_account_email)
632+
signature = _sign_message(
633+
string_to_sign, access_token, service_account_email, universe_domain
634+
)
627635
signature_bytes = base64.b64decode(signature)
628636
signature = binascii.hexlify(signature_bytes).decode("ascii")
629637
else:
@@ -647,7 +655,12 @@ def get_v4_now_dtstamps():
647655
return timestamp, datestamp
648656

649657

650-
def _sign_message(message, access_token, service_account_email):
658+
def _sign_message(
659+
message,
660+
access_token,
661+
service_account_email,
662+
universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
663+
):
651664
"""Signs a message.
652665
653666
:type message: str
@@ -669,17 +682,22 @@ def _sign_message(message, access_token, service_account_email):
669682
message = _helpers._to_bytes(message)
670683

671684
method = "POST"
672-
url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:signBlob?alt=json".format(
673-
service_account_email
674-
)
685+
url = f"https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{service_account_email}:signBlob?alt=json"
675686
headers = {
676687
"Authorization": "Bearer " + access_token,
677688
"Content-type": "application/json",
678689
}
679690
body = json.dumps({"payload": base64.b64encode(message).decode("utf-8")})
680-
681691
request = requests.Request()
682-
response = request(url=url, method=method, body=body, headers=headers)
692+
693+
def retriable_request():
694+
response = request(url=url, method=method, body=body, headers=headers)
695+
return response
696+
697+
# Apply the default retry object to the signBlob call.
698+
retry = DEFAULT_RETRY
699+
call = retry(retriable_request)
700+
response = call()
683701

684702
if response.status != http.client.OK:
685703
raise exceptions.TransportError(

google/cloud/storage/blob.py

+4
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,9 @@ def generate_signed_url(
607607
client = self._require_client(client) # May be redundant, but that's ok.
608608
credentials = client._credentials
609609

610+
client = self._require_client(client)
611+
universe_domain = client.universe_domain
612+
610613
if version == "v2":
611614
helper = generate_signed_url_v2
612615
else:
@@ -638,6 +641,7 @@ def generate_signed_url(
638641
query_parameters=query_parameters,
639642
service_account_email=service_account_email,
640643
access_token=access_token,
644+
universe_domain=universe_domain,
641645
)
642646

643647
@create_trace_span(name="Storage.Blob.exists")

tests/system/conftest.py

+30
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,33 @@ def universe_domain_client(
384384
)
385385
with contextlib.closing(ud_storage_client):
386386
yield ud_storage_client
387+
388+
389+
@pytest.fixture(scope="function")
390+
def universe_domain_bucket(universe_domain_client, test_universe_location):
391+
bucket_name = _helpers.unique_name("gcp-systest-ud")
392+
bucket = universe_domain_client.create_bucket(
393+
bucket_name, location=test_universe_location
394+
)
395+
396+
blob = bucket.blob("README.txt")
397+
blob.upload_from_string(_helpers.signing_blob_content)
398+
399+
yield bucket
400+
401+
_helpers.delete_bucket(bucket)
402+
403+
404+
@pytest.fixture(scope="function")
405+
def universe_domain_iam_client(
406+
test_universe_domain, test_universe_project_id, universe_domain_credential
407+
):
408+
from google.cloud import iam_credentials_v1
409+
410+
client_options = {"universe_domain": test_universe_domain}
411+
iam_client = iam_credentials_v1.IAMCredentialsClient(
412+
credentials=universe_domain_credential,
413+
client_options=client_options,
414+
)
415+
416+
return iam_client

tests/system/test__signing.py

+29
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,35 @@ def test_create_signed_read_url_v4_w_access_token(
287287
)
288288

289289

290+
def test_create_signed_read_url_v4_w_access_token_universe_domain(
291+
universe_domain_iam_client,
292+
universe_domain_client,
293+
test_universe_location,
294+
universe_domain_credential,
295+
universe_domain_bucket,
296+
no_mtls,
297+
):
298+
service_account_email = universe_domain_credential.service_account_email
299+
name = path_template.expand(
300+
"projects/{project}/serviceAccounts/{service_account}",
301+
project="-",
302+
service_account=service_account_email,
303+
)
304+
scope = [
305+
"https://www.googleapis.com/auth/devstorage.read_write",
306+
"https://www.googleapis.com/auth/iam",
307+
]
308+
response = universe_domain_iam_client.generate_access_token(name=name, scope=scope)
309+
310+
_create_signed_read_url_helper(
311+
universe_domain_client,
312+
universe_domain_bucket,
313+
version="v4",
314+
service_account_email=service_account_email,
315+
access_token=response.access_token,
316+
)
317+
318+
290319
def _create_signed_delete_url_helper(client, bucket, version="v2", expiration=None):
291320
expiration = _morph_expiration(version, expiration)
292321

tests/unit/test_blob.py

+3
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ def _generate_signed_url_helper(
487487
expected_creds = credentials
488488
client = self._make_client(_credentials=object())
489489

490+
expected_universe_domain = client.universe_domain
491+
490492
bucket = _Bucket(client)
491493
blob = self._make_one(blob_name, bucket=bucket, encryption_key=encryption_key)
492494

@@ -564,6 +566,7 @@ def _generate_signed_url_helper(
564566
"query_parameters": query_parameters,
565567
"access_token": access_token,
566568
"service_account_email": service_account_email,
569+
"universe_domain": expected_universe_domain,
567570
}
568571
signer.assert_called_once_with(expected_creds, **expected_kwargs)
569572

0 commit comments

Comments
 (0)