Skip to content

Commit 134e3af

Browse files
macastelaznbayati
andauthored
feat(auth): Add blocking Regional Access Boundary Lookup and Seed Support (#16720)
In order for the gcloud CLI to support Regional Access Boundary, the Python auth SDK needs to support blocking lookups as well as allowing an initial seed RAB to be provided (gcloud will set this seed if the CLI has a locally cached valid RAB available). Additional details can be found at [go/rab-python-gcloud-one-pager](https://docs.google.com/document/d/1PvwqXp-jznleIpA8UzXU9of6sMvHPZA8HN_TSSsWFcc/edit?resourcekey=0-yQowO9bsBsHdv_60zQsKnA&tab=t.0) --------- Co-authored-by: nbayati <99771966+nbayati@users.noreply.github.com>
1 parent ad72180 commit 134e3af

10 files changed

Lines changed: 438 additions & 102 deletions

File tree

packages/google-auth/google/auth/_regional_access_boundary_utils.py

Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@
2020
import logging
2121
import os
2222
import threading
23-
from typing import NamedTuple, Optional
23+
from typing import NamedTuple, Optional, TYPE_CHECKING
2424

2525
from google.auth import _helpers
2626
from google.auth import environment_vars
2727

28+
if TYPE_CHECKING:
29+
import google.auth.credentials
30+
import google.auth.transport
31+
2832
_LOGGER = logging.getLogger(__name__)
2933

3034

@@ -97,6 +101,7 @@ def __init__(self):
97101
)
98102
self.refresh_manager = _RegionalAccessBoundaryRefreshManager()
99103
self._update_lock = threading.Lock()
104+
self._use_blocking_regional_access_boundary_lookup = False
100105

101106
def __getstate__(self):
102107
"""Pickle helper that serializes the _update_lock attribute."""
@@ -109,6 +114,43 @@ def __setstate__(self, state):
109114
self.__dict__.update(state)
110115
self._update_lock = threading.Lock()
111116

117+
def __eq__(self, other):
118+
"""Checks if two managers are equal."""
119+
if not isinstance(other, _RegionalAccessBoundaryManager):
120+
return NotImplemented
121+
return (
122+
self._data == other._data
123+
and self._use_blocking_regional_access_boundary_lookup
124+
== other._use_blocking_regional_access_boundary_lookup
125+
)
126+
127+
def enable_blocking_lookup(self):
128+
"""Enables blocking Regional Access Boundary lookup.
129+
130+
When enabled, the Regional Access Boundary lookup will be performed
131+
synchronously in the calling thread instead of asynchronously in a
132+
background thread.
133+
"""
134+
self._use_blocking_regional_access_boundary_lookup = True
135+
136+
def set_initial_regional_access_boundary(self, encoded_locations=None, expiry=None):
137+
"""Manually sets the regional access boundary to the client provided seed.
138+
139+
Args:
140+
encoded_locations (Optional[str]): The encoded locations string.
141+
expiry (Optional[datetime.datetime]): The expiry time for the boundary.
142+
If encoded_locations is not provided, expiry is ignored.
143+
"""
144+
if not encoded_locations:
145+
expiry = None
146+
147+
self._data = _RegionalAccessBoundaryData(
148+
encoded_locations=encoded_locations,
149+
expiry=expiry,
150+
cooldown_expiry=None,
151+
cooldown_duration=DEFAULT_REGIONAL_ACCESS_BOUNDARY_COOLDOWN,
152+
)
153+
112154
def apply_headers(self, headers):
113155
"""Applies the Regional Access Boundary header to the provided dictionary.
114156
@@ -151,48 +193,50 @@ def maybe_start_refresh(self, credentials, request):
151193
return
152194

153195
# If all checks pass, start the background refresh.
154-
self.refresh_manager.start_refresh(credentials, request, self)
155-
156-
157-
class _RegionalAccessBoundaryRefreshThread(threading.Thread):
158-
"""Thread for background refreshing of the Regional Access Boundary."""
196+
if self._use_blocking_regional_access_boundary_lookup:
197+
self.start_blocking_refresh(credentials, request)
198+
else:
199+
self.refresh_manager.start_refresh(credentials, request, self)
159200

160-
def __init__(self, credentials, request, rab_manager):
161-
super().__init__()
162-
self.daemon = True
163-
self._credentials = credentials
164-
self._request = request
165-
self._rab_manager = rab_manager
201+
def start_blocking_refresh(self, credentials, request):
202+
"""Initiates a blocking lookup of the Regional Access Boundary.
166203
167-
def run(self):
168-
"""
169-
Performs the Regional Access Boundary lookup and updates the state.
204+
If the lookup raises an exception, it is caught and logged as a warning,
205+
and the lookup is treated as a failure (entering cooldown). Exceptions
206+
are not propagated to the caller.
170207
171-
This method is run in a separate thread. It delegates the actual lookup
172-
to the credentials object's `_lookup_regional_access_boundary` method.
173-
Based on the lookup's outcome (success or complete failure after retries),
174-
it updates the cached Regional Access Boundary information,
175-
its expiry, its cooldown expiry, and its exponential cooldown duration.
208+
Args:
209+
credentials (google.auth.credentials.Credentials): The credentials to refresh.
210+
request (google.auth.transport.Request): The object used to make HTTP requests.
176211
"""
177-
# Catch exceptions (e.g., from the underlying transport) to prevent the
178-
# background thread from crashing. This ensures we can gracefully enter
179-
# an exponential cooldown state on failure.
180212
try:
213+
# The fail_fast parameter is set to True to ensure we don't block the calling
214+
# thread for too long. This will do two things: 1) set a timeout to 3s
215+
# instead of the default 120s and 2) ensure we do not retry at all
181216
regional_access_boundary_info = (
182-
self._credentials._lookup_regional_access_boundary(self._request)
217+
credentials._lookup_regional_access_boundary(request, fail_fast=True)
183218
)
184219
except Exception as e:
185220
if _helpers.is_logging_enabled(_LOGGER):
186221
_LOGGER.warning(
187-
"Asynchronous Regional Access Boundary lookup raised an exception: %s",
222+
"Blocking Regional Access Boundary lookup raised an exception: %s",
188223
e,
189224
exc_info=True,
190225
)
191226
regional_access_boundary_info = None
192227

193-
with self._rab_manager._update_lock:
228+
self.process_regional_access_boundary_info(regional_access_boundary_info)
229+
230+
def process_regional_access_boundary_info(self, regional_access_boundary_info):
231+
"""Processes the regional access boundary info and updates the state.
232+
233+
Args:
234+
regional_access_boundary_info (Optional[Mapping[str, str]]): The regional access
235+
boundary info to process.
236+
"""
237+
with self._update_lock:
194238
# Capture the current state before calculating updates.
195-
current_data = self._rab_manager._data
239+
current_data = self._data
196240

197241
if regional_access_boundary_info:
198242
# On success, update the boundary and its expiry, and clear any cooldown.
@@ -206,14 +250,12 @@ def run(self):
206250
cooldown_duration=DEFAULT_REGIONAL_ACCESS_BOUNDARY_COOLDOWN,
207251
)
208252
if _helpers.is_logging_enabled(_LOGGER):
209-
_LOGGER.debug(
210-
"Asynchronous Regional Access Boundary lookup successful."
211-
)
253+
_LOGGER.debug("Regional Access Boundary lookup successful.")
212254
else:
213255
# On failure, calculate cooldown and update state.
214256
if _helpers.is_logging_enabled(_LOGGER):
215257
_LOGGER.warning(
216-
"Asynchronous Regional Access Boundary lookup failed. Entering cooldown."
258+
"Regional Access Boundary lookup failed. Entering cooldown."
217259
)
218260

219261
next_cooldown_expiry = (
@@ -241,7 +283,53 @@ def run(self):
241283
)
242284

243285
# Perform the atomic swap of the state object.
244-
self._rab_manager._data = updated_data
286+
self._data = updated_data
287+
288+
289+
class _RegionalAccessBoundaryRefreshThread(threading.Thread):
290+
"""Thread for background refreshing of the Regional Access Boundary."""
291+
292+
def __init__(
293+
self,
294+
credentials: "google.auth.credentials.CredentialsWithRegionalAccessBoundary", # noqa: F821
295+
request: "google.auth.transport.Request", # noqa: F821
296+
rab_manager: "_RegionalAccessBoundaryManager",
297+
):
298+
super().__init__()
299+
self.daemon = True
300+
self._credentials = credentials
301+
self._request = request
302+
self._rab_manager = rab_manager
303+
304+
def run(self):
305+
"""
306+
Performs the Regional Access Boundary lookup and updates the state.
307+
308+
This method is run in a separate thread. It delegates the actual lookup
309+
to the credentials object's `_lookup_regional_access_boundary` method.
310+
Based on the lookup's outcome (success or complete failure after retries),
311+
it updates the cached Regional Access Boundary information,
312+
its expiry, its cooldown expiry, and its exponential cooldown duration.
313+
"""
314+
# Catch exceptions (e.g., from the underlying transport) to prevent the
315+
# background thread from crashing. This ensures we can gracefully enter
316+
# an exponential cooldown state on failure.
317+
try:
318+
regional_access_boundary_info = (
319+
self._credentials._lookup_regional_access_boundary(self._request)
320+
)
321+
except Exception as e:
322+
if _helpers.is_logging_enabled(_LOGGER):
323+
_LOGGER.warning(
324+
"Asynchronous Regional Access Boundary lookup raised an exception: %s",
325+
e,
326+
exc_info=True,
327+
)
328+
regional_access_boundary_info = None
329+
330+
self._rab_manager.process_regional_access_boundary_info(
331+
regional_access_boundary_info
332+
)
245333

246334

247335
class _RegionalAccessBoundaryRefreshManager(object):

packages/google-auth/google/auth/credentials.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,40 @@ def _copy_regional_access_boundary_manager(self, target):
361361
new_manager._data = self._rab_manager._data
362362
target._rab_manager = new_manager
363363

364+
def _with_regional_access_boundary(self, seed):
365+
"""Returns a copy of these credentials with the the regional_access_boundary
366+
set to the provided seed. This is intended for internal use only as invalid
367+
seeds would produce unexpected results until automatic recovery is supported.
368+
Currently this is used by the gcloud CLI and therefore changes to the
369+
contract MUST be backwards compatible (e.g. the method signature must be
370+
unchanged and a copy of the credenials with the RAB set must be returned).
371+
372+
373+
Returns:
374+
google.auth.credentials.Credentials: A new credentials instance.
375+
"""
376+
creds = self._make_copy()
377+
creds._rab_manager.set_initial_regional_access_boundary(
378+
encoded_locations=seed.get("encodedLocations", None),
379+
expiry=seed.get("expiry", None),
380+
)
381+
return creds
382+
383+
def _with_blocking_regional_access_boundary_lookup(self):
384+
"""Returns a copy of these credentials with the blocking lookup mode enabled.
385+
This is intended for internal use only as blocking lookup requires additional
386+
care and consideration. Currently this is used by the gcloud CLI and
387+
therefore changes to the contract MUST be backwards compatible (e.g. the
388+
method signature must be unchanged and a copy of the credentials with the
389+
blocking lookup flag set to true must be returned).
390+
391+
Returns:
392+
google.auth.credentials.Credentials: A new credentials instance.
393+
"""
394+
creds = self._make_copy()
395+
creds._rab_manager.enable_blocking_lookup()
396+
return creds
397+
364398
def _maybe_start_regional_access_boundary_refresh(self, request, url):
365399
"""
366400
Starts a background thread to refresh the Regional Access Boundary if needed.
@@ -421,11 +455,16 @@ def before_request(self, request, method, url, headers):
421455
"""Refreshes the access token and triggers the Regional Access Boundary
422456
lookup if necessary.
423457
"""
424-
super(CredentialsWithRegionalAccessBoundary, self).before_request(
425-
request, method, url, headers
426-
)
458+
if self._use_non_blocking_refresh:
459+
self._non_blocking_refresh(request)
460+
else:
461+
self._blocking_refresh(request)
462+
427463
self._maybe_start_regional_access_boundary_refresh(request, url)
428464

465+
metrics.add_metric_header(headers, self._metric_header_for_usage())
466+
self.apply(headers)
467+
429468
def refresh(self, request):
430469
"""Refreshes the access token.
431470
@@ -435,13 +474,16 @@ def refresh(self, request):
435474
self._perform_refresh_token(request)
436475

437476
def _lookup_regional_access_boundary(
438-
self, request: "google.auth.transport.Request" # noqa: F821
477+
self,
478+
request: "google.auth.transport.Request", # noqa: F821
479+
fail_fast: bool = False,
439480
) -> "Optional[Dict[str, str]]":
440481
"""Calls the Regional Access Boundary lookup API to retrieve the Regional Access Boundary information.
441482
442483
Args:
443484
request (google.auth.transport.Request): The object used to make
444485
HTTP requests.
486+
fail_fast (bool): Whether the lookup should fail fast (short timeout, no retries).
445487
446488
Returns:
447489
Optional[Dict[str, str]]: The Regional Access Boundary information returned by the lookup API, or None if the lookup failed.
@@ -456,7 +498,9 @@ def _lookup_regional_access_boundary(
456498
headers: Dict[str, str] = {}
457499
self._apply(headers)
458500
self._rab_manager.apply_headers(headers)
459-
return _client._lookup_regional_access_boundary(request, url, headers=headers)
501+
return _client._lookup_regional_access_boundary(
502+
request, url, headers=headers, fail_fast=fail_fast
503+
)
460504

461505
@abc.abstractmethod
462506
def _build_regional_access_boundary_lookup_url(

packages/google-auth/google/oauth2/_client.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
_JSON_CONTENT_TYPE = "application/json"
4444
_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
4545
_REFRESH_GRANT_TYPE = "refresh_token"
46+
_BLOCKING_REGIONAL_ACCESS_BOUNDARY_LOOKUP_TIMEOUT = 3
4647

4748

4849
def _handle_error_response(response_data, retryable_error):
@@ -517,7 +518,7 @@ def refresh_grant(
517518
return _handle_refresh_grant_response(response_data, refresh_token)
518519

519520

520-
def _lookup_regional_access_boundary(request, url, headers=None):
521+
def _lookup_regional_access_boundary(request, url, headers=None, fail_fast=False):
521522
"""Implements the global lookup of a credential Regional Access Boundary.
522523
For the lookup, we send a request to the global lookup endpoint and then
523524
parse the response. Service account credentials, workload identity
@@ -527,6 +528,7 @@ def _lookup_regional_access_boundary(request, url, headers=None):
527528
HTTP requests.
528529
url (str): The Regional Access Boundary lookup url.
529530
headers (Optional[Mapping[str, str]]): The headers for the request.
531+
fail_fast (bool): Whether the lookup should fail fast (uses a short timeout and no retries).
530532
Returns:
531533
Optional[Mapping[str,list|str]]: A dictionary containing
532534
"locations" as a list of allowed locations as strings and
@@ -541,7 +543,7 @@ def _lookup_regional_access_boundary(request, url, headers=None):
541543
"""
542544

543545
response_data = _lookup_regional_access_boundary_request(
544-
request, url, headers=headers
546+
request, url, headers=headers, fail_fast=fail_fast
545547
)
546548
if response_data is None:
547549
# Error was already logged by _lookup_regional_access_boundary_request
@@ -557,7 +559,7 @@ def _lookup_regional_access_boundary(request, url, headers=None):
557559

558560

559561
def _lookup_regional_access_boundary_request(
560-
request, url, can_retry=True, headers=None
562+
request, url, can_retry=True, headers=None, fail_fast=False
561563
):
562564
"""Makes a request to the Regional Access Boundary lookup endpoint.
563565
@@ -567,6 +569,7 @@ def _lookup_regional_access_boundary_request(
567569
url (str): The Regional Access Boundary lookup url.
568570
can_retry (bool): Enable or disable request retry behavior. Defaults to true.
569571
headers (Optional[Mapping[str, str]]): The headers for the request.
572+
fail_fast (bool): Whether the lookup should fail fast (uses a short timeout and no retries).
570573
571574
Returns:
572575
Optional[Mapping[str, str]]: The JSON-decoded response data on success, or None on failure.
@@ -576,7 +579,7 @@ def _lookup_regional_access_boundary_request(
576579
response_data,
577580
retryable_error,
578581
) = _lookup_regional_access_boundary_request_no_throw(
579-
request, url, can_retry, headers
582+
request, url, can_retry=can_retry, headers=headers, fail_fast=fail_fast
580583
)
581584
if not response_status_ok:
582585
_LOGGER.warning(
@@ -589,7 +592,7 @@ def _lookup_regional_access_boundary_request(
589592

590593

591594
def _lookup_regional_access_boundary_request_no_throw(
592-
request, url, can_retry=True, headers=None
595+
request, url, can_retry=True, headers=None, fail_fast=False
593596
):
594597
"""Makes a request to the Regional Access Boundary lookup endpoint. This
595598
function doesn't throw on response errors.
@@ -600,6 +603,7 @@ def _lookup_regional_access_boundary_request_no_throw(
600603
url (str): The Regional Access Boundary lookup url.
601604
can_retry (bool): Enable or disable request retry behavior. Defaults to true.
602605
headers (Optional[Mapping[str, str]]): The headers for the request.
606+
fail_fast (bool): Whether the lookup should fail fast (uses a short timeout and no retries).
603607
604608
Returns:
605609
Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
@@ -611,9 +615,12 @@ def _lookup_regional_access_boundary_request_no_throw(
611615
response_data = {}
612616
retryable_error = False
613617

614-
retries = _exponential_backoff.ExponentialBackoff(total_attempts=6)
618+
timeout = _BLOCKING_REGIONAL_ACCESS_BOUNDARY_LOOKUP_TIMEOUT if fail_fast else None
619+
total_attempts = 1 if fail_fast else 6
620+
retries = _exponential_backoff.ExponentialBackoff(total_attempts=total_attempts)
621+
615622
for _ in retries:
616-
response = request(method="GET", url=url, headers=headers)
623+
response = request(method="GET", url=url, headers=headers, timeout=timeout)
617624
response_body = (
618625
response.data.decode("utf-8")
619626
if hasattr(response.data, "decode")

0 commit comments

Comments
 (0)