From 629f61fe0445bf67b2932b4a90e38b6f1a132c69 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 16 Jun 2026 11:10:17 -0700 Subject: [PATCH] feat(Gax): add support for Regional Access Boundaries --- Gax/src/CredentialsWrapper.php | 22 +++++++---- Gax/src/GapicClientTrait.php | 12 +++++- Gax/tests/Unit/CredentialsWrapperTest.php | 28 ++++++++++++++ Gax/tests/Unit/GapicClientTraitTest.php | 45 +++++++++++++++++++++++ 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/Gax/src/CredentialsWrapper.php b/Gax/src/CredentialsWrapper.php index d6e90892b5a3..ff9ccc9c24d7 100644 --- a/Gax/src/CredentialsWrapper.php +++ b/Gax/src/CredentialsWrapper.php @@ -58,7 +58,6 @@ class CredentialsWrapper implements HeaderCredentialsInterface, ProjectIdProvide /** @var callable $authHttpHandle */ private $authHttpHandler; - private string $universeDomain; private bool $hasCheckedUniverse = false; /** @var int */ @@ -76,14 +75,13 @@ class CredentialsWrapper implements HeaderCredentialsInterface, ProjectIdProvide public function __construct( FetchAuthTokenInterface $credentialsFetcher, ?callable $authHttpHandler = null, - string $universeDomain = GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN + private string $universeDomain = GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, ) { $this->credentialsFetcher = $credentialsFetcher; $this->authHttpHandler = $authHttpHandler; if (empty($universeDomain)) { throw new ValidationException('The universe domain cannot be empty'); } - $this->universeDomain = $universeDomain; } /** @@ -116,6 +114,9 @@ public function __construct( * @type bool $useJwtAccessWithScope * Ensures service account credentials use JWT Access (also known as self-signed * JWTs), even when user-defined scopes are supplied. + * @type bool $enableRegionalAccessBoundary + * Enable the Regional Access Boundary lookup in the credentials which sets the + * `x-allowed-locations` header in the request. * } * @param string $universeDomain The expected universe of the credentials. Defaults to * "googleapis.com" @@ -136,6 +137,7 @@ public static function build( 'quotaProject' => null, 'defaultScopes' => null, 'useJwtAccessWithScope' => true, + 'enableRegionalAccessBoundary' => false, ]; $keyFile = $args['keyFile']; @@ -147,7 +149,8 @@ public static function build( $args['authCacheOptions'], $args['authCache'], $args['quotaProject'], - $args['defaultScopes'] + $args['defaultScopes'], + $args['enableRegionalAccessBoundary'], ); if ($loader instanceof FetchAuthTokenCache) { $loader = $loader->getFetcher(); @@ -167,7 +170,8 @@ public static function build( $loader = CredentialsLoader::makeCredentials( $args['scopes'], $keyFile, - $args['defaultScopes'] + $args['defaultScopes'], + $args['enableRegionalAccessBoundary'], ); } @@ -322,7 +326,8 @@ private static function buildApplicationDefaultCredentials( ?array $authCacheOptions = null, ?CacheItemPoolInterface $authCache = null, $quotaProject = null, - ?array $defaultScopes = null + ?array $defaultScopes = null, + bool $enableRegionalAccessBoundary = true, ) { try { return ApplicationDefaultCredentials::getCredentials( @@ -331,7 +336,10 @@ private static function buildApplicationDefaultCredentials( $authCacheOptions, $authCache, $quotaProject, - $defaultScopes + $defaultScopes, + null, // $universeDomain + null, // $logger + $enableRegionalAccessBoundary, ); } catch (DomainException $ex) { throw new ValidationException('Could not construct ApplicationDefaultCredentials', $ex->getCode(), $ex); diff --git a/Gax/src/GapicClientTrait.php b/Gax/src/GapicClientTrait.php index 3bd91ece34b0..b269bb78baa7 100644 --- a/Gax/src/GapicClientTrait.php +++ b/Gax/src/GapicClientTrait.php @@ -344,10 +344,18 @@ private function setClientOptions(array $options) $options['credentialsConfig']['quotaProject'] ?? null ); } else { + $enableRegionalAccessBoundary = filter_var( + getenv('GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT'), + FILTER_VALIDATE_BOOLEAN + ); + $isRegional = str_ends_with($options['apiEndpoint'], '.rep.googleapis.com') + || str_ends_with($options['apiEndpoint'], '.rep.sandbox.googleapis.com'); $this->credentialsWrapper = $this->createCredentialsWrapper( $options['credentials'], - $options['credentialsConfig'], - $options['universeDomain'] + $options['credentialsConfig'] + [ + 'enableRegionalAccessBoundary' => $enableRegionalAccessBoundary && !$isRegional + ], + $options['universeDomain'], ); } diff --git a/Gax/tests/Unit/CredentialsWrapperTest.php b/Gax/tests/Unit/CredentialsWrapperTest.php index d7d5a1c0979b..428957daf1b1 100644 --- a/Gax/tests/Unit/CredentialsWrapperTest.php +++ b/Gax/tests/Unit/CredentialsWrapperTest.php @@ -145,6 +145,34 @@ public function buildDataWithoutExplicitKeyFile() quotaProject: $quotaProject )), ], + [ + ['enableRegionalAccessBoundary' => true], + new CredentialsWrapper(ApplicationDefaultCredentials::getCredentials( + null, + $authHttpHandler, + null, + $defaultAuthCache, + null, + null, + null, + null, + true, // $enableRegionalAccessBoundary + )), + ], + [ + ['enableRegionalAccessBoundary' => false], + new CredentialsWrapper(ApplicationDefaultCredentials::getCredentials( + null, + $authHttpHandler, + null, + $defaultAuthCache, + null, + null, + null, + null, + false, // $enableRegionalAccessBoundary + )), + ], ]; $this->setEnv('GOOGLE_APPLICATION_CREDENTIALS', $appDefaultCreds); diff --git a/Gax/tests/Unit/GapicClientTraitTest.php b/Gax/tests/Unit/GapicClientTraitTest.php index fef2951d4197..51369ba18bab 100644 --- a/Gax/tests/Unit/GapicClientTraitTest.php +++ b/Gax/tests/Unit/GapicClientTraitTest.php @@ -896,6 +896,51 @@ public function setClientOptionsData() ]; } + /** + * @dataProvider buildClientOptionsRegionalEndpointData + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testBuildClientOptionsRegionalEndpoint(?string $apiEndpoint, ?string $envVarValue, bool $rabEnabled) + { + if ($envVarValue !== null) { + putenv('GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT=' . $envVarValue); + } + + $client = new class() extends StubGapicClient { + public array $capturedCredentialsConfig = []; + + public function createCredentialsWrapper($credentials, array $credentialsConfig, string $universeDomain) + { + $this->capturedCredentialsConfig = $credentialsConfig; + return null; + } + }; + + $clientOptions = $client->buildClientOptions( + $apiEndpoint !== null ? ['apiEndpoint' => $apiEndpoint] : [] + ); + $client->setClientOptions($clientOptions); + + $this->assertEquals( + $rabEnabled, + $client->capturedCredentialsConfig['enableRegionalAccessBoundary'] ?? null + ); + } + + public function buildClientOptionsRegionalEndpointData() + { + return [ + ['test.googleapis.com', 'true', true], + ['test.rep.googleapis.com', 'true', false], + ['test.rep.sandbox.googleapis.com', 'true', false], + ['test.googleapis.com', 'false', false], + ['test.googleapis.com', null, false], + ['', 'true', true], // Empty endpoint case + [null, 'true', true], // Unset endpoint case + ]; + } + /** * @dataProvider buildRequestHeaderParams */