Skip to content

Commit af19393

Browse files
authored
feat(auth): make RAB feature production ready (#17390)
This PR resolves issues identified during verification of gcloud Regional Access Boundary (RAB) flows and enables RAB verification by default: * Removes the client-side environment variable feature gate (`GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED`) to execute RAB lookups by default across standard credential classes. * Updates the Python auth SDK to recognize mTLS regional endpoints (`.rep.mtls.googleapis.com`), bypassing redundant RAB lookups on secure transport boundaries. * Defers Service Account impersonation setup until HTTP request execution before_request, propagating active cached tokens downward onto the inner credential to guarantee that access tokens restored across external CLI entrypoints correctly delegate regional access boundary (RAB) lookups to target Service Account endpoints without forcing redundant STS network renewal.
1 parent 00ec9bf commit af19393

12 files changed

Lines changed: 318 additions & 201 deletions

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@
2020
import functools
2121
import inspect
2222
import logging
23-
import os
2423
import threading
2524
from typing import NamedTuple, Optional, TYPE_CHECKING
2625

2726
from google.auth import _helpers
28-
from google.auth import environment_vars
2927

3028
if TYPE_CHECKING: # pragma: NO COVER
3129
import google.auth.credentials
@@ -34,25 +32,6 @@
3432
_LOGGER = logging.getLogger(__name__)
3533

3634

37-
@functools.lru_cache()
38-
def is_regional_access_boundary_enabled():
39-
"""Checks if Regional Access Boundary is enabled via environment variable.
40-
41-
The environment variable is interpreted as a boolean with the following
42-
(case-insensitive) rules:
43-
- "true", "1" are considered true.
44-
- Any other value (or unset) is considered false.
45-
46-
Returns:
47-
bool: True if Regional Access Boundary is enabled, False otherwise.
48-
"""
49-
value = os.environ.get(environment_vars.GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED)
50-
if value is None:
51-
return False
52-
53-
return value.lower() in ("true", "1")
54-
55-
5635
# The default lifetime for a cached Regional Access Boundary.
5736
DEFAULT_REGIONAL_ACCESS_BOUNDARY_TTL = datetime.timedelta(hours=6)
5837

packages/google-auth/google/auth/aws.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -841,11 +841,9 @@ def from_info(cls, info, **kwargs):
841841
Raises:
842842
ValueError: For invalid parameters.
843843
"""
844-
aws_security_credentials_supplier = info.get(
845-
"aws_security_credentials_supplier"
846-
)
847-
kwargs.update(
848-
{"aws_security_credentials_supplier": aws_security_credentials_supplier}
844+
kwargs.setdefault(
845+
"aws_security_credentials_supplier",
846+
info.get("aws_security_credentials_supplier"),
849847
)
850848
return super(Credentials, cls).from_info(info, **kwargs)
851849

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,13 @@ def _is_regional_endpoint(self, url):
446446
try:
447447
# Do not perform a lookup if the request is for a regional endpoint.
448448
hostname = urlparse(url).hostname
449-
if hostname and (
450-
hostname.endswith(".rep.googleapis.com")
451-
or hostname.endswith(".rep.sandbox.googleapis.com")
449+
if hostname and hostname.endswith(
450+
(
451+
".rep.googleapis.com",
452+
".rep.sandbox.googleapis.com",
453+
".rep.mtls.googleapis.com",
454+
".rep.mtls.sandbox.googleapis.com",
455+
)
452456
):
453457
return True
454458
except (ValueError, TypeError, AttributeError):
@@ -484,16 +488,11 @@ def _maybe_start_regional_access_boundary_refresh(self, request, url):
484488
def _is_regional_access_boundary_lookup_required(self):
485489
"""Checks if a Regional Access Boundary lookup is required.
486490
487-
A lookup is required if the feature is enabled via an environment
488-
variable and the universe domain is supported.
491+
A lookup is required if the universe domain is supported.
489492
490493
Returns:
491494
bool: True if a Regional Access Boundary lookup is required, False otherwise.
492495
"""
493-
# Check if the feature is enabled.
494-
if not _regional_access_boundary_utils.is_regional_access_boundary_enabled():
495-
return False
496-
497496
# Skip for non-default universe domains.
498497
if self.universe_domain != DEFAULT_UNIVERSE_DOMAIN:
499498
return False

packages/google-auth/google/auth/environment_vars.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@
105105
AWS_REGION = "AWS_REGION"
106106
AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"
107107

108+
108109
GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED = "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED"
109110
"""Environment variable controlling whether to enable trust boundary feature.
110-
The default value is false. Users have to explicitly set this value to true."""
111+
112+
.. deprecated::
113+
This environment variable is deprecated and no longer has any effect.
114+
"""
111115

112116
GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG"
113117
"""Environment variable defining the location of Google API certificate config

packages/google-auth/google/auth/external_account.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import json
3737
import logging
3838
import re
39+
import threading
3940
from typing import Optional, TYPE_CHECKING
4041

4142

@@ -200,6 +201,7 @@ def __init__(
200201
self._metrics_options = self._create_default_metrics_options()
201202

202203
self._impersonated_credentials = None
204+
self._impersonation_lock = threading.Lock()
203205
self._project_id = None
204206
self._supplier_context = SupplierContext(
205207
self._subject_token_type, self._audience
@@ -213,6 +215,15 @@ def __init__(
213215
"credentials"
214216
)
215217

218+
def __getstate__(self):
219+
state = self.__dict__.copy()
220+
state.pop("_impersonation_lock", None)
221+
return state
222+
223+
def __setstate__(self, state):
224+
super().__setstate__(state)
225+
self._impersonation_lock = threading.Lock()
226+
216227
@property
217228
def info(self):
218229
"""Generates the dictionary representation of the current credentials.
@@ -444,6 +455,17 @@ def _maybe_start_regional_access_boundary_refresh(self, request, url):
444455
HTTP requests.
445456
url (str): The URL of the request.
446457
"""
458+
if self._should_initialize_impersonated_credentials():
459+
with self._impersonation_lock:
460+
if self._impersonated_credentials is None:
461+
impersonated = self._initialize_impersonated_credentials()
462+
if getattr(self, "token", None):
463+
impersonated.token = self.token
464+
if getattr(self, "expiry", None):
465+
impersonated.expiry = self.expiry
466+
self._impersonated_credentials = impersonated
467+
self._rab_manager = impersonated._rab_manager
468+
447469
if getattr(self, "_impersonated_credentials", None):
448470
self._impersonated_credentials._maybe_start_regional_access_boundary_refresh(
449471
request, url
@@ -462,7 +484,11 @@ def _perform_refresh_token(self, request, cert_fingerprint=None):
462484
)
463485

464486
if self._should_initialize_impersonated_credentials():
465-
self._impersonated_credentials = self._initialize_impersonated_credentials()
487+
with self._impersonation_lock:
488+
if self._impersonated_credentials is None:
489+
self._impersonated_credentials = (
490+
self._initialize_impersonated_credentials()
491+
)
466492

467493
if self._impersonated_credentials:
468494
self._impersonated_credentials.refresh(request)
@@ -581,9 +607,10 @@ def with_universe_domain(self, universe_domain):
581607
return cred
582608

583609
def _should_initialize_impersonated_credentials(self):
610+
"""Determines if the underlying Service Account credential should be initialized."""
584611
return (
585-
self._service_account_impersonation_url is not None
586-
and self._impersonated_credentials is None
612+
getattr(self, "_service_account_impersonation_url", None) is not None
613+
and getattr(self, "_impersonated_credentials", None) is None
587614
)
588615

589616
def _initialize_impersonated_credentials(self):

packages/google-auth/google/auth/identity_pool.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,7 @@ def from_info(cls, info, **kwargs):
526526
Raises:
527527
ValueError: For invalid parameters.
528528
"""
529-
subject_token_supplier = info.get("subject_token_supplier")
530-
kwargs.update({"subject_token_supplier": subject_token_supplier})
529+
kwargs.setdefault("subject_token_supplier", info.get("subject_token_supplier"))
531530
return super(Credentials, cls).from_info(info, **kwargs)
532531

533532
@classmethod

packages/google-auth/tests/compute_engine/test_credentials.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
import base64
1515
import datetime
16+
import re
1617
from unittest import mock
1718

1819
import pytest # type: ignore
@@ -202,6 +203,7 @@ def test_before_request_refreshes(self, get):
202203
"access_token": "token",
203204
"expires_in": 500,
204205
},
206+
"googleapis.com",
205207
]
206208

207209
# Credentials should start as invalid
@@ -248,7 +250,11 @@ def test_with_universe_domain(self):
248250
assert creds.universe_domain == "universe_domain"
249251
assert creds._universe_domain_cached
250252

251-
def test_token_usage_metrics(self):
253+
@mock.patch(
254+
"google.auth.compute_engine._metadata.get_universe_domain",
255+
return_value="googleapis.com",
256+
)
257+
def test_token_usage_metrics(self, mock_get_universe_domain):
252258
self.credentials.token = "token"
253259
self.credentials.expiry = None
254260

@@ -406,11 +412,7 @@ def test_build_regional_access_boundary_lookup_url_no_email(
406412
url = creds._build_regional_access_boundary_lookup_url()
407413
assert url is None
408414

409-
@mock.patch(
410-
"google.auth._regional_access_boundary_utils.is_regional_access_boundary_enabled",
411-
return_value=True,
412-
)
413-
def test_is_regional_access_boundary_lookup_required(self, mock_enabled):
415+
def test_is_regional_access_boundary_lookup_required(self):
414416
creds = self.credentials
415417
creds._universe_domain_cached = True
416418

@@ -438,15 +440,11 @@ def test_build_regional_access_boundary_lookup_url_with_invalid_email(self):
438440
url = creds._build_regional_access_boundary_lookup_url()
439441
assert url is None
440442

441-
@mock.patch(
442-
"google.auth._regional_access_boundary_utils.is_regional_access_boundary_enabled",
443-
return_value=True,
444-
)
445443
@mock.patch(
446444
"google.auth.compute_engine._metadata.get_service_account_info", autospec=True
447445
)
448446
def test_regional_access_boundary_disabled_state_transitions(
449-
self, mock_get_service_account_info, mock_enabled
447+
self, mock_get_service_account_info
450448
):
451449
mock_get_service_account_info.return_value = {
452450
"email": "spiffe://trust-domain/ns/ns/sa/sa",
@@ -765,6 +763,15 @@ def test_with_target_audience_integration(self):
765763
json={},
766764
)
767765

766+
# mock allowedLocations for Regional Access Boundary
767+
responses.add(
768+
responses.GET,
769+
re.compile(r".*/allowedLocations$"),
770+
status=200,
771+
content_type="application/json",
772+
json={"encodedLocations": "0xABC"},
773+
)
774+
768775
# mock token for credentials
769776
responses.add(
770777
responses.GET,
@@ -783,8 +790,10 @@ def test_with_target_audience_integration(self):
783790
signature = base64.b64encode(b"some-signature").decode("utf-8")
784791
responses.add(
785792
responses.POST,
786-
"https://iamcredentials.googleapis.com/v1/projects/-/"
787-
"serviceAccounts/service-account@example.com:signBlob",
793+
re.compile(
794+
r"https://iamcredentials\.(mtls\.)?googleapis\.com/v1/projects/-/"
795+
r"serviceAccounts/service-account@example\.com:signBlob"
796+
),
788797
status=200,
789798
content_type="application/json",
790799
json={"keyId": "some-key-id", "signedBlob": signature},
@@ -947,12 +956,23 @@ def test_with_quota_project_integration(self):
947956
json={},
948957
)
949958

959+
# mock allowedLocations for Regional Access Boundary
960+
responses.add(
961+
responses.GET,
962+
re.compile(r".*/allowedLocations$"),
963+
status=200,
964+
content_type="application/json",
965+
json={"encodedLocations": "0xABC"},
966+
)
967+
950968
# mock sign blob endpoint
951969
signature = base64.b64encode(b"some-signature").decode("utf-8")
952970
responses.add(
953971
responses.POST,
954-
"https://iamcredentials.googleapis.com/v1/projects/-/"
955-
"serviceAccounts/service-account@example.com:signBlob",
972+
re.compile(
973+
r"https://iamcredentials\.(mtls\.)?googleapis\.com/v1/projects/-/"
974+
r"serviceAccounts/service-account@example\.com:signBlob"
975+
),
956976
status=200,
957977
content_type="application/json",
958978
json={"keyId": "some-key-id", "signedBlob": signature},

0 commit comments

Comments
 (0)