From fe3929702475283ef1bfd8f4d142004535c22fbf Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 6 Dec 2023 13:59:04 -0800 Subject: [PATCH 01/11] feat: add universe domain support for bigquery and storage --- BigQuery/src/Connection/Rest.php | 11 +++++++-- Core/composer.json | 2 +- Core/src/RequestWrapper.php | 8 +++++-- Core/src/RequestWrapperTrait.php | 34 +++++++++++++++++++++++++++ Core/src/RestTrait.php | 40 ++++++++++++++++++++++++++------ Storage/src/Connection/Rest.php | 11 +++++++-- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/BigQuery/src/Connection/Rest.php b/BigQuery/src/Connection/Rest.php index 7214284798b0..39768e29bf7c 100644 --- a/BigQuery/src/Connection/Rest.php +++ b/BigQuery/src/Connection/Rest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\BigQuery\Connection; +use Google\Auth\GetUniverseDomainInterface; use Google\Cloud\BigQuery\BigQueryClient; use Google\Cloud\BigQuery\Connection\ConnectionInterface; use Google\Cloud\Core\RequestBuilder; @@ -43,8 +44,13 @@ class Rest implements ConnectionInterface */ const BASE_URI = 'https://www.googleapis.com/bigquery/v2/'; + /** + * @deprecated + */ const DEFAULT_API_ENDPOINT = 'https://bigquery.googleapis.com'; + private const DEFAULT_API_ENDPOINT_TEMPLATE = 'https://bigquery.UNIVERSE_DOMAIN'; + /** * @deprecated */ @@ -65,10 +71,11 @@ public function __construct(array $config = []) $config += [ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/bigquery-v2.json', 'componentVersion' => BigQueryClient::VERSION, - 'apiEndpoint' => self::DEFAULT_API_ENDPOINT + 'apiEndpoint' => null, + 'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, ]; - $apiEndpoint = $this->getApiEndpoint(self::DEFAULT_API_ENDPOINT, $config); + $apiEndpoint = $this->getApiEndpoint(null, $config, self::DEFAULT_API_ENDPOINT_TEMPLATE); $this->setRequestWrapper(new RequestWrapper($config)); $this->setRequestBuilder(new RequestBuilder( diff --git a/Core/composer.json b/Core/composer.json index 63aff87d114c..4a6f1361d94f 100644 --- a/Core/composer.json +++ b/Core/composer.json @@ -6,7 +6,7 @@ "require": { "php": ">=7.4", "rize/uri-template": "~0.3", - "google/auth": "^1.18", + "google/auth": "^1.33", "guzzlehttp/guzzle": "^6.5.8|^7.4.4", "guzzlehttp/promises": "^1.4||^2.0", "guzzlehttp/psr7": "^1.7|^2.0", diff --git a/Core/src/RequestWrapper.php b/Core/src/RequestWrapper.php index 79c2f142ea48..6f9f4a8a0317 100644 --- a/Core/src/RequestWrapper.php +++ b/Core/src/RequestWrapper.php @@ -20,6 +20,7 @@ use Google\Auth\FetchAuthTokenCache; use Google\Auth\FetchAuthTokenInterface; use Google\Auth\GetQuotaProjectInterface; +use Google\Auth\GetUniverseDomainInterface; use Google\Auth\HttpHandler\Guzzle6HttpHandler; use Google\Auth\HttpHandler\HttpHandlerFactory; use Google\Auth\UpdateMetadataInterface; @@ -140,7 +141,8 @@ public function __construct(array $config = []) 'componentVersion' => null, 'restRetryFunction' => null, 'restDelayFunction' => null, - 'restCalcDelayFunction' => null + 'restCalcDelayFunction' => null, + 'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, ]; $this->componentVersion = $config['componentVersion']; @@ -155,6 +157,7 @@ public function __construct(array $config = []) $this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build(); $this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler; $this->asyncHttpHandler = $config['asyncHttpHandler'] ?: $this->buildDefaultAsyncHandler(); + $this->universeDomain = $config['universeDomain']; if ($this->credentialsFetcher instanceof AnonymousCredentials) { $this->shouldSignRequest = false; @@ -311,11 +314,12 @@ private function applyHeaders(RequestInterface $request, array $options = []) if ($this->shouldSignRequest) { $quotaProject = $this->quotaProject; + $credentialsFetcher = $this->accessToken ? null : $this->getCredentialsFetcher(); + $this->checkUniverseDomain($credentialsFetcher); if ($this->accessToken) { $request = $request->withHeader('authorization', 'Bearer ' . $this->accessToken); } else { - $credentialsFetcher = $this->getCredentialsFetcher(); $request = $this->addAuthHeaders($request, $credentialsFetcher); if ($credentialsFetcher instanceof GetQuotaProjectInterface) { diff --git a/Core/src/RequestWrapperTrait.php b/Core/src/RequestWrapperTrait.php index 17cf45287bd1..21d67f60d770 100644 --- a/Core/src/RequestWrapperTrait.php +++ b/Core/src/RequestWrapperTrait.php @@ -18,11 +18,13 @@ namespace Google\Cloud\Core; use Google\Auth\ApplicationDefaultCredentials; +use Google\Auth\GetUniverseDomainInterface; use Google\Auth\Cache\MemoryCacheItemPool; use Google\Auth\Credentials\ServiceAccountCredentials; use Google\Auth\CredentialsLoader; use Google\Auth\FetchAuthTokenCache; use Google\Auth\FetchAuthTokenInterface; +use Google\Cloud\Core\GoogleException; use Psr\Cache\CacheItemPoolInterface; /** @@ -73,6 +75,9 @@ trait RequestWrapperTrait */ private $quotaProject; + private string $universeDomain; + private bool $hasCheckedUniverse = false; + /** * Sets common defaults between request wrappers. * @@ -203,6 +208,35 @@ public function getCredentialsFetcher() } } + /** + * Verify that the expected universe domain matches the universe domain from the credentials. + */ + private function checkUniverseDomain(FetchAuthTokenInterface $credentialsFetcher = null) + { + if (false === $this->hasCheckedUniverse) { + if (is_null($credentialsFetcher)) { + if ($this->universeDomain !== GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN) { + throw new GoogleException(sprintf( + 'The accessToken option is not supported outside of the default universe domain (%s).', + GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN + )); + } + } else { + $credentialsUniverse = $this->credentialsFetcher instanceof GetUniverseDomainInterface + ? $this->credentialsFetcher->getUniverseDomain() + : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; + if ($credentialsUniverse !== $this->universeDomain) { + throw new GoogleException(sprintf( + 'The configured universe domain (%s) does not match the credential universe domain (%s)', + $this->universeDomain, + $credentialsUniverse + )); + } + } + $this->hasCheckedUniverse = true; + } + } + /** * Returns application default credentials. Abstracted out for unit testing. * diff --git a/Core/src/RestTrait.php b/Core/src/RestTrait.php index e2fa52b62333..809987efefcc 100644 --- a/Core/src/RestTrait.php +++ b/Core/src/RestTrait.php @@ -19,6 +19,7 @@ use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Exception\ServiceException; +use UnexpectedValueException; /** * Provides shared functionality for REST service implementations. @@ -118,20 +119,45 @@ public function send($resource, $method, array $options = [], $whitelisted = fal * * @param string $default * @param array $config + * @param string $apiEndpointTemplate * @return string */ - private function getApiEndpoint($default, array $config) + private function getApiEndpoint($default, array $config, string $apiEndpointTemplate = null) { - $res = $config['apiEndpoint'] ?? $default; + // If the $default parameter is provided, or the user has set an "apiEndoint" config option, + // fall back to the previous behavior. + if ($res = $config['apiEndpoint'] ?? $default) { + if (substr($res, -1) !== '/') { + $res = $res . '/'; + } + + if (strpos($res, '//') === false) { + $res = 'https://' . $res; + } + + return $res; + } - if (substr($res, -1) !== '/') { - $res = $res . '/'; + // One of the $default or the $template must always be set + if (!$apiEndpointTemplate) { + throw new UnexpectedValueException( + 'A default API endpoint must be provided if no "apiEndpoint" config option is set.' + ); } - if (strpos($res, '//') === false) { - $res = 'https://' . $res; + if (!isset($config['universeDomain'])) { + throw new UnexpectedValueException( + 'The "universeDomain" config value must be set to use the default API endpoint template.' + ); } - return $res; + $apiEndpoint = str_replace( + 'UNIVERSE_DOMAIN', + $config['universeDomain'], + $apiEndpointTemplate + ); + + // Preserve the behavior of guaranteeing a trailing "/" + return $apiEndpoint . (substr($apiEndpoint, -1) !== '/' ? '/' : ''); } } diff --git a/Storage/src/Connection/Rest.php b/Storage/src/Connection/Rest.php index b32ed878def0..e4108bc47016 100644 --- a/Storage/src/Connection/Rest.php +++ b/Storage/src/Connection/Rest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Storage\Connection; +use Google\Auth\GetUniverseDomainInterface; use Google\Cloud\Core\RequestBuilder; use Google\Cloud\Core\RequestWrapper; use Google\Cloud\Core\RestTrait; @@ -64,8 +65,13 @@ class Rest implements ConnectionInterface */ const BASE_URI = 'https://storage.googleapis.com/storage/v1/'; + /** + * @deprecated + */ const DEFAULT_API_ENDPOINT = 'https://storage.googleapis.com'; + const DEFAULT_API_ENDPOINT_TEMPLATE = 'https://storage.UNIVERSE_DOMAIN'; + /** * @deprecated */ @@ -104,13 +110,14 @@ public function __construct(array $config = []) $config += [ 'serviceDefinitionPath' => __DIR__ . '/ServiceDefinition/storage-v1.json', 'componentVersion' => StorageClient::VERSION, - 'apiEndpoint' => self::DEFAULT_API_ENDPOINT, + 'apiEndpoint' => null, + 'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, // Cloud Storage needs to provide a default scope because the Storage // API does not accept JWTs with "audience" 'scopes' => StorageClient::FULL_CONTROL_SCOPE, ]; - $this->apiEndpoint = $this->getApiEndpoint(self::DEFAULT_API_ENDPOINT, $config); + $this->apiEndpoint = $this->getApiEndpoint(null, $config, self::DEFAULT_API_ENDPOINT_TEMPLATE); $this->setRequestWrapper(new RequestWrapper($config)); $this->setRequestBuilder(new RequestBuilder( From 338bd703dc682716c3823f1f5da66b71b46489e2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 7 Dec 2023 08:26:34 -0800 Subject: [PATCH 02/11] fix static analysis --- Core/src/RequestWrapperTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/src/RequestWrapperTrait.php b/Core/src/RequestWrapperTrait.php index 21d67f60d770..973f81681403 100644 --- a/Core/src/RequestWrapperTrait.php +++ b/Core/src/RequestWrapperTrait.php @@ -24,7 +24,7 @@ use Google\Auth\CredentialsLoader; use Google\Auth\FetchAuthTokenCache; use Google\Auth\FetchAuthTokenInterface; -use Google\Cloud\Core\GoogleException; +use Google\Cloud\Core\Exception\GoogleException; use Psr\Cache\CacheItemPoolInterface; /** From e19507ed0ceb7ffe9ca202f9cf23073cfacb5267 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 13 Dec 2023 15:14:00 -0800 Subject: [PATCH 03/11] ensure universe domain is checked even when shouldSignRequest is false --- Core/src/RequestWrapper.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Core/src/RequestWrapper.php b/Core/src/RequestWrapper.php index 6f9f4a8a0317..29b55afd72d8 100644 --- a/Core/src/RequestWrapper.php +++ b/Core/src/RequestWrapper.php @@ -314,12 +314,16 @@ private function applyHeaders(RequestInterface $request, array $options = []) if ($this->shouldSignRequest) { $quotaProject = $this->quotaProject; - $credentialsFetcher = $this->accessToken ? null : $this->getCredentialsFetcher(); - $this->checkUniverseDomain($credentialsFetcher); if ($this->accessToken) { + // if an access token is provided, check the universe domain against "googleapis.com" + $this->checkUniverseDomain(null); $request = $request->withHeader('authorization', 'Bearer ' . $this->accessToken); } else { + // if a credentials fetcher is provided, check the universe domain against the + // credential's universe domain + $credentialsFetcher = $this->getCredentialsFetcher(); + $this->checkUniverseDomain($credentialsFetcher); $request = $this->addAuthHeaders($request, $credentialsFetcher); if ($credentialsFetcher instanceof GetQuotaProjectInterface) { @@ -330,6 +334,9 @@ private function applyHeaders(RequestInterface $request, array $options = []) if ($quotaProject) { $request = $request->withHeader('X-Goog-User-Project', $quotaProject); } + } else { + // If we are not signing the request, check the universe domain against "googleapis.com" + $this->checkUniverseDomain(null); } return $request; From 00236f996539d663133695d6ded74fbce35cb06d Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 14 Dec 2023 07:37:27 -0800 Subject: [PATCH 04/11] update root google/auth version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a3313cfd528b..2dd9b24815c1 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "ramsey/uuid": "^4.0", "google/gax": "^1.24.0", "google/common-protos": "^4.0", - "google/auth": "^1.18" + "google/auth": "^1.33" }, "require-dev": { "phpunit/phpunit": "^9.0", From e9363d3656676f293b07de94defea71df193d050 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 19 Dec 2023 07:58:24 -0800 Subject: [PATCH 05/11] Update Core/src/RestTrait.php Co-authored-by: Vishwaraj Anand --- Core/src/RestTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/src/RestTrait.php b/Core/src/RestTrait.php index 809987efefcc..70966a8a0359 100644 --- a/Core/src/RestTrait.php +++ b/Core/src/RestTrait.php @@ -141,7 +141,7 @@ private function getApiEndpoint($default, array $config, string $apiEndpointTemp // One of the $default or the $template must always be set if (!$apiEndpointTemplate) { throw new UnexpectedValueException( - 'A default API endpoint must be provided if no "apiEndpoint" config option is set.' + 'An API endpoint template must be provided if no "apiEndpoint" or default endpoint is set.' ); } From cf81f37d955c2ada3d7ecbf94c2a50e5a8641f82 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 2 Jan 2024 10:46:24 -0800 Subject: [PATCH 06/11] use passed in wrapper --- Core/src/RequestWrapperTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/src/RequestWrapperTrait.php b/Core/src/RequestWrapperTrait.php index 973f81681403..39c69bcc0dd5 100644 --- a/Core/src/RequestWrapperTrait.php +++ b/Core/src/RequestWrapperTrait.php @@ -222,8 +222,8 @@ private function checkUniverseDomain(FetchAuthTokenInterface $credentialsFetcher )); } } else { - $credentialsUniverse = $this->credentialsFetcher instanceof GetUniverseDomainInterface - ? $this->credentialsFetcher->getUniverseDomain() + $credentialsUniverse = $credentialsFetcher instanceof GetUniverseDomainInterface + ? $credentialsFetcher->getUniverseDomain() : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; if ($credentialsUniverse !== $this->universeDomain) { throw new GoogleException(sprintf( From c56b46610375d0b3eeba44c010e157c7ff0b0458 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 2 Jan 2024 11:14:39 -0800 Subject: [PATCH 07/11] add pubsub support --- Core/src/GrpcTrait.php | 12 +++++++++--- PubSub/src/Connection/Grpc.php | 10 +++++++++- PubSub/src/Connection/Rest.php | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Core/src/GrpcTrait.php b/Core/src/GrpcTrait.php index 48d42f67ca1b..144be7b71bbc 100644 --- a/Core/src/GrpcTrait.php +++ b/Core/src/GrpcTrait.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Core; +use Google\Auth\GetUniverseDomainInterface; use Google\ApiCore\CredentialsWrapper; use Google\Cloud\Core\ArrayTrait; use Google\Cloud\Core\Duration; @@ -94,10 +95,14 @@ public function send(callable $request, array $args, $whitelisted = false) * * @param string $version * @param callable|null $authHttpHandler + * @param string|null $universeDomain * @return array */ - private function getGaxConfig($version, callable $authHttpHandler = null) - { + private function getGaxConfig( + $version, + callable $authHttpHandler = null, + string $universeDomain = null + ) { $config = [ 'libName' => 'gccl', 'libVersion' => $version, @@ -110,7 +115,8 @@ private function getGaxConfig($version, callable $authHttpHandler = null) if (class_exists(CredentialsWrapper::class)) { $config['credentials'] = new CredentialsWrapper( $this->requestWrapper->getCredentialsFetcher(), - $authHttpHandler + $authHttpHandler, + $universeDomain ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN ); } else { $config += [ diff --git a/PubSub/src/Connection/Grpc.php b/PubSub/src/Connection/Grpc.php index 24e272716af7..cfa08d0be3bf 100644 --- a/PubSub/src/Connection/Grpc.php +++ b/PubSub/src/Connection/Grpc.php @@ -56,6 +56,9 @@ class Grpc implements ConnectionInterface use EmulatorTrait; use GrpcTrait; + /** + * @deprecated + */ const BASE_URI = 'https://pubsub.googleapis.com/'; const COMPRESSION_HEADER_KEY = 'grpc-internal-encoding-request'; @@ -112,7 +115,8 @@ public function __construct(array $config = []) PubSubClient::VERSION, isset($config['authHttpHandler']) ? $config['authHttpHandler'] - : null + : null, + $config['universeDomain'] ?? null ); $config += ['emulatorHost' => null]; @@ -127,6 +131,10 @@ public function __construct(array $config = []) $this->emulatorGapicConfig($config['emulatorHost']) ); } + + if (isset($config['universeDomain'])) { + $grpcConfig['universeDomain'] = $config['universeDomain']; + } //@codeCoverageIgnoreEnd $this->clientConfig = $grpcConfig; diff --git a/PubSub/src/Connection/Rest.php b/PubSub/src/Connection/Rest.php index eb7db695b740..828077095480 100644 --- a/PubSub/src/Connection/Rest.php +++ b/PubSub/src/Connection/Rest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\PubSub\Connection; +use Google\Auth\GetUniverseDomainInterface; use Google\Cloud\Core\EmulatorTrait; use Google\Cloud\Core\RequestBuilder; use Google\Cloud\Core\RequestWrapper; @@ -39,19 +40,28 @@ class Rest implements ConnectionInterface use RestTrait; use UriTrait; + /** + * @deprecated + */ const BASE_URI = 'https://pubsub.googleapis.com/'; + private const BASE_URI_TEMPLATE = 'https://pubsub.UNIVERSE_DOMAIN'; + /** * @param array $config */ public function __construct(array $config = []) { - $config += ['emulatorHost' => null]; + $config += [ + 'emulatorHost' => null, + 'universeDomain' => GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, + ]; - $baseUri = $this->getApiEndpoint(self::BASE_URI, $config); if ((bool) $config['emulatorHost']) { $baseUri = $this->emulatorBaseUri($config['emulatorHost']); $config['shouldSignRequest'] = false; + } else { + $baseUri = $this->getApiEndpoint(null, $config, self::BASE_URI_TEMPLATE); } $config += [ From b2a73a27db1d9f6eda0d3efd6036e0121d25646a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 2 Jan 2024 14:03:49 -0800 Subject: [PATCH 08/11] adds RequestWrapper tests --- Core/src/RequestWrapper.php | 44 ++++++++++++ Core/src/RequestWrapperTrait.php | 31 --------- Core/tests/Unit/RequestWrapperTest.php | 93 ++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 31 deletions(-) diff --git a/Core/src/RequestWrapper.php b/Core/src/RequestWrapper.php index 29b55afd72d8..c2ef21d71708 100644 --- a/Core/src/RequestWrapper.php +++ b/Core/src/RequestWrapper.php @@ -26,6 +26,7 @@ use Google\Auth\UpdateMetadataInterface; use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Core\RequestWrapperTrait; +use Google\Cloud\Core\Exception\GoogleException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Utils; @@ -95,6 +96,16 @@ class RequestWrapper */ private $calcDelayFunction; + /** + * @var string The universe domain to verify against the credentials. + */ + private string $universeDomain; + + /** + * @var bool Ensure we only check the universe domain once. + */ + private bool $hasCheckedUniverse = false; + /** * @param array $config [optional] { * Configuration options. Please see @@ -126,6 +137,7 @@ class RequestWrapper * @type callable $restCalcDelayFunction Sets the conditions for * determining how long to wait between attempts to retry. Function * signature should match: `function (int $attempt) : int`. + * @type string $universerDomain The expected universe of the credentials. Defaults to "googleapis.com". * } */ public function __construct(array $config = []) @@ -495,4 +507,36 @@ private function buildDefaultAsyncHandler() ? [$this->httpHandler, 'async'] : [HttpHandlerFactory::build(), 'async']; } + + /** + * Verify that the expected universe domain matches the universe domain from the credentials. + */ + private function checkUniverseDomain(FetchAuthTokenInterface $credentialsFetcher = null) + { + if (false === $this->hasCheckedUniverse) { + if ($this->universeDomain === '') { + throw new GoogleException('The universe domain cannot be empty.'); + } + if (is_null($credentialsFetcher)) { + if ($this->universeDomain !== GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN) { + throw new GoogleException(sprintf( + 'The accessToken option is not supported outside of the default universe domain (%s).', + GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN + )); + } + } else { + $credentialsUniverse = $credentialsFetcher instanceof GetUniverseDomainInterface + ? $credentialsFetcher->getUniverseDomain() + : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; + if ($credentialsUniverse !== $this->universeDomain) { + throw new GoogleException(sprintf( + 'The configured universe domain (%s) does not match the credential universe domain (%s)', + $this->universeDomain, + $credentialsUniverse + )); + } + } + $this->hasCheckedUniverse = true; + } + } } diff --git a/Core/src/RequestWrapperTrait.php b/Core/src/RequestWrapperTrait.php index 39c69bcc0dd5..a4c498d78f50 100644 --- a/Core/src/RequestWrapperTrait.php +++ b/Core/src/RequestWrapperTrait.php @@ -18,13 +18,11 @@ namespace Google\Cloud\Core; use Google\Auth\ApplicationDefaultCredentials; -use Google\Auth\GetUniverseDomainInterface; use Google\Auth\Cache\MemoryCacheItemPool; use Google\Auth\Credentials\ServiceAccountCredentials; use Google\Auth\CredentialsLoader; use Google\Auth\FetchAuthTokenCache; use Google\Auth\FetchAuthTokenInterface; -use Google\Cloud\Core\Exception\GoogleException; use Psr\Cache\CacheItemPoolInterface; /** @@ -208,35 +206,6 @@ public function getCredentialsFetcher() } } - /** - * Verify that the expected universe domain matches the universe domain from the credentials. - */ - private function checkUniverseDomain(FetchAuthTokenInterface $credentialsFetcher = null) - { - if (false === $this->hasCheckedUniverse) { - if (is_null($credentialsFetcher)) { - if ($this->universeDomain !== GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN) { - throw new GoogleException(sprintf( - 'The accessToken option is not supported outside of the default universe domain (%s).', - GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN - )); - } - } else { - $credentialsUniverse = $credentialsFetcher instanceof GetUniverseDomainInterface - ? $credentialsFetcher->getUniverseDomain() - : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; - if ($credentialsUniverse !== $this->universeDomain) { - throw new GoogleException(sprintf( - 'The configured universe domain (%s) does not match the credential universe domain (%s)', - $this->universeDomain, - $credentialsUniverse - )); - } - } - $this->hasCheckedUniverse = true; - } - } - /** * Returns application default credentials. Abstracted out for unit testing. * diff --git a/Core/tests/Unit/RequestWrapperTest.php b/Core/tests/Unit/RequestWrapperTest.php index 0aa264975928..0b5af7058395 100644 --- a/Core/tests/Unit/RequestWrapperTest.php +++ b/Core/tests/Unit/RequestWrapperTest.php @@ -21,6 +21,7 @@ use Google\Auth\Credentials\ServiceAccountCredentials; use Google\Auth\FetchAuthTokenCache; use Google\Auth\FetchAuthTokenInterface; +use Google\Auth\GetUniverseDomainInterface; use Google\Auth\UpdateMetadataInterface; use Google\Cloud\Core\AnonymousCredentials; use Google\Cloud\Core\Exception\BadRequestException; @@ -776,6 +777,98 @@ public function testFetchingCredentialAsAuthHeaderWithOverlappingHeaders() ); } + /** + * @dataProvider provideCheckUniverseDomainFails + */ + public function testCheckUniverseDomainFails( + ?string $universeDomain, + ?string $credentialsUniverse, + string $message = null + ) { + $this->expectException(GoogleException::class); + $this->expectExceptionMessage($message ?: sprintf( + 'The configured universe domain (%s) does not match the credential universe domain (%s)', + is_null($universeDomain) ? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN : $universeDomain, + is_null($credentialsUniverse) ? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN : $credentialsUniverse, + )); + $fetcher = $this->prophesize(FetchAuthTokenInterface::class); + // When the $credentialsUniverse is null, the fetcher doesn't implement GetUniverseDomainInterface + if (!is_null($credentialsUniverse)) { + $fetcher->willImplement(GetUniverseDomainInterface::class); + $fetcher->getUniverseDomain()->willReturn($credentialsUniverse); + } + $fetcher->getLastReceivedToken()->willReturn(null); + + $config = ['credentialsFetcher' => $fetcher->reveal()]; + // A null value here represents not passing in a universeDomain + if (!is_null($universeDomain)) { + $config['universeDomain'] = $universeDomain; + } + $requestWrapper = new RequestWrapper($config); + // Send a fake request + $requestWrapper->send(new Request('GET', 'http://www.example.com')); + } + + public function provideCheckUniverseDomainFails() + { + return [ + ['foo.com', 'googleapis.com'], + ['googleapis.com', 'foo.com'], + ['googleapis.com', ''], + ['', 'googleapis.com', 'The universe domain cannot be empty'], + [null, 'foo.com'], // null in RequestWrapper will default to "googleapis.com" + ['foo.com', null], // Credentials not implementing GetUniverseDomainInterface will default to "googleapis.com" + ]; + } + + /** + * @dataProvider provideCheckUniverseDomainPasses + */ + public function testCheckUniverseDomainPasses(?string $universeDomain, ?string $credentialsUniverse) + { + $fetcher = $this->prophesize(FetchAuthTokenInterface::class); + // When the $credentialsUniverse is null, the fetcher doesn't implement GetUniverseDomainInterface + if (!is_null($credentialsUniverse)) { + $fetcher->willImplement(GetUniverseDomainInterface::class); + $fetcher->getUniverseDomain()->shouldBeCalledOnce()->willReturn($credentialsUniverse); + } + $fetcher->getLastReceivedToken()->willReturn(null); + $fetcher->fetchAuthToken(Argument::any())->willReturn(['access_token' => 'abc']); + $fetcher->getCacheKey() + ->shouldBeCalledTimes(2) + ->willReturn(null); + + $called = false; + $config = [ + 'credentialsFetcher' => $fetcher->reveal(), + 'httpHandler' => function (Request $request) use (&$called) { + $headers = $request->getHeaders(); + $this->assertArrayHasKey('authorization', $headers); + $this->assertEquals('Bearer abc', $headers['authorization'][0]); + $called = true; + } + ]; + // A null value here represents not passing in a universeDomain + if (!is_null($universeDomain)) { + $config['universeDomain'] = $universeDomain; + } + $requestWrapper = new RequestWrapper($config); + + // send a fake request + $requestWrapper->send(new Request('GET', 'http://www.example.com')); + $this->assertTrue($called); + } + + public function provideCheckUniverseDomainPasses() + { + return [ + [null, 'googleapis.com'], // null will default to "googleapis.com" + ['foo.com', 'foo.com'], + ['googleapis.com', 'googleapis.com'], + ['googleapis.com', null], + ]; + } + private function prophesizeUpdateMetadataFetcher($credentialsFetcher) { // We have to mock this message because RequestWrapper wraps the credentials using the From 339e7619fd3f4b7834594321ec9184752ccc1ffc Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 3 Jan 2024 09:19:43 -0800 Subject: [PATCH 09/11] Update Core/src/RequestWrapper.php Co-authored-by: Vishwaraj Anand --- Core/src/RequestWrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/src/RequestWrapper.php b/Core/src/RequestWrapper.php index c2ef21d71708..eb8f7650284a 100644 --- a/Core/src/RequestWrapper.php +++ b/Core/src/RequestWrapper.php @@ -137,7 +137,7 @@ class RequestWrapper * @type callable $restCalcDelayFunction Sets the conditions for * determining how long to wait between attempts to retry. Function * signature should match: `function (int $attempt) : int`. - * @type string $universerDomain The expected universe of the credentials. Defaults to "googleapis.com". + * @type string $universeDomain The expected universe of the credentials. Defaults to "googleapis.com". * } */ public function __construct(array $config = []) From 1bedd0edeed0392493168592a21149d2970f9eef Mon Sep 17 00:00:00 2001 From: Vishwaraj Anand Date: Thu, 4 Jan 2024 13:54:46 +0530 Subject: [PATCH 10/11] fix: adding unit tests for connection classes (#6934) --- BigQuery/tests/Unit/Connection/RestTest.php | 29 +++++++++++++++------ Core/tests/Unit/RequestWrapperTest.php | 2 +- Core/tests/Unit/RestTraitTest.php | 28 ++++++++++++++++++++ PubSub/tests/Unit/Connection/GrpcTest.php | 24 +++++++++++++++++ PubSub/tests/Unit/Connection/RestTest.php | 29 +++++++++++++++------ Storage/tests/Unit/Connection/RestTest.php | 29 +++++++++++++++------ 6 files changed, 116 insertions(+), 25 deletions(-) diff --git a/BigQuery/tests/Unit/Connection/RestTest.php b/BigQuery/tests/Unit/Connection/RestTest.php index 369d57d3f5ce..90432941e206 100644 --- a/BigQuery/tests/Unit/Connection/RestTest.php +++ b/BigQuery/tests/Unit/Connection/RestTest.php @@ -28,6 +28,7 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; +use UnexpectedValueException; /** * @group bigquery @@ -45,21 +46,33 @@ public function setUp(): void $this->successBody = '{"canI":"kickIt"}'; } - public function testApiEndpoint() + /** + * @dataProvider clientUniverseDomainConfigProvider + */ + public function testApiEndpointForUniverseDomain($config, $expectedEndpoint, $expectException = false) { - $endpoint = 'https://foobar.com/'; - $rest = TestHelpers::stub(Rest::class, [ - [ - 'apiEndpoint' => $endpoint - ] - ], ['requestBuilder']); + if ($expectException) { + $this->expectException(UnexpectedValueException::class); + } + $rest = TestHelpers::stub(Rest::class, [$config], ['requestBuilder']); $rb = $rest->___getProperty('requestBuilder'); $r = new \ReflectionObject($rb); $p = $r->getProperty('baseUri'); $p->setAccessible(true); - $this->assertEquals($endpoint . 'bigquery/v2/', $p->getValue($rb)); + $this->assertEquals($expectedEndpoint, $p->getValue($rb)); + } + + public function clientUniverseDomainConfigProvider() + { + return [ + [[], 'https://bigquery.googleapis.com/bigquery/v2/'], // default + [['apiEndpoint' => 'https://foobar.com'], 'https://foobar.com/bigquery/v2/'], + [['universeDomain' => 'googleapis.com'], 'https://bigquery.googleapis.com/bigquery/v2/'], + [['universeDomain' => 'abc.def.ghi'], 'https://bigquery.abc.def.ghi/bigquery/v2/'], + [['universeDomain' => null], '', true], + ]; } /** diff --git a/Core/tests/Unit/RequestWrapperTest.php b/Core/tests/Unit/RequestWrapperTest.php index 0b5af7058395..1ac68f1dad2a 100644 --- a/Core/tests/Unit/RequestWrapperTest.php +++ b/Core/tests/Unit/RequestWrapperTest.php @@ -817,7 +817,7 @@ public function provideCheckUniverseDomainFails() ['googleapis.com', ''], ['', 'googleapis.com', 'The universe domain cannot be empty'], [null, 'foo.com'], // null in RequestWrapper will default to "googleapis.com" - ['foo.com', null], // Credentials not implementing GetUniverseDomainInterface will default to "googleapis.com" + ['foo.com', null], // Credentials not implementing `GetUniverseDomainInterface` will have default universe ]; } diff --git a/Core/tests/Unit/RestTraitTest.php b/Core/tests/Unit/RestTraitTest.php index 191b7477aff3..031a056dd85a 100644 --- a/Core/tests/Unit/RestTraitTest.php +++ b/Core/tests/Unit/RestTraitTest.php @@ -28,6 +28,7 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; +use UnexpectedValueException; /** * @group core @@ -172,6 +173,21 @@ public function testGetApiEndpoint($input = null, $expected = null) ); } + /** + * @dataProvider universeDomains + */ + public function testGetApiEndpointFromUniverseDomain($config, $template, $expected = null, $expectException = false) + { + if ($expectException) { + $this->expectException(UnexpectedValueException::class); + } + + $this->assertEquals( + $expected, + TestHelpers::impl(RestTrait::class)->call('getApiEndpoint', [null, $config, $template]) + ); + } + public function testAppendsPrettyPrintParameter() { $requestBuilder = $this->prophesize(RequestBuilder::class); @@ -196,6 +212,18 @@ public function testAppendsPrettyPrintParameter() $this->assertEquals('prettyPrint=false', $this->implementation->send('foo', 'bar', [])); } + public function universeDomains() + { + return [ + [[], '', null, true], + [[], null, null, true], + [['universeDomain' => null], 'ab.cd', null, true], + [['universeDomain' => ''], 'ab.cd/', 'ab.cd/'], + [['universeDomain' => 'defg'], '//ab.cd//', '//ab.cd//'], + [['universeDomain' => 'defg'], 'ab.UNIVERSE_DOMAIN.cd', 'ab.defg.cd/'], + ]; + } + public function endpoints() { return [ diff --git a/PubSub/tests/Unit/Connection/GrpcTest.php b/PubSub/tests/Unit/Connection/GrpcTest.php index 8bce29276621..3f392d2444bf 100644 --- a/PubSub/tests/Unit/Connection/GrpcTest.php +++ b/PubSub/tests/Unit/Connection/GrpcTest.php @@ -77,6 +77,30 @@ public function testApiEndpoint() $this->assertEquals($expected, $grpc->config['apiEndpoint']); } + + /** + * @dataProvider clientUniverseDomainConfigProvider + */ + public function testUniverseDomain($config, $expectedUniverseDomain, $domainConfigExists = true) + { + $grpc = new GrpcStub($config); + + if ($domainConfigExists) { + $this->assertEquals($expectedUniverseDomain, $grpc->config['universeDomain']); + } else { + $this->assertArrayNotHasKey('universeDomain', $grpc->config); + } + } + + public function clientUniverseDomainConfigProvider() + { + return [ + [[], 'googleapis.com', false], + [['universeDomain' => 'googleapis.com'], 'googleapis.com'], + [['universeDomain' => 'abc.def.ghi'], 'abc.def.ghi'], + ]; + } + public function testUpdateTopic() { $topic = new Topic(); diff --git a/PubSub/tests/Unit/Connection/RestTest.php b/PubSub/tests/Unit/Connection/RestTest.php index 4e58ad16c5a0..c2cabef93cfe 100644 --- a/PubSub/tests/Unit/Connection/RestTest.php +++ b/PubSub/tests/Unit/Connection/RestTest.php @@ -27,6 +27,7 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; +use UnexpectedValueException; /** * @group pubsub @@ -46,21 +47,33 @@ public function setUp(): void $this->successBody = '{"canI":"kickIt"}'; } - public function testApiEndpoint() + /** + * @dataProvider clientUniverseDomainConfigProvider + */ + public function testApiEndpointForUniverseDomain($config, $expectedEndpoint, $expectException = false) { - $endpoint = 'https://foobar.com/'; - $rest = TestHelpers::stub(Rest::class, [ - [ - 'apiEndpoint' => $endpoint - ] - ], ['requestBuilder']); + if ($expectException) { + $this->expectException(UnexpectedValueException::class); + } + $rest = TestHelpers::stub(Rest::class, [$config], ['requestBuilder']); $rb = $rest->___getProperty('requestBuilder'); $r = new \ReflectionObject($rb); $p = $r->getProperty('baseUri'); $p->setAccessible(true); - $this->assertEquals($endpoint, $p->getValue($rb)); + $this->assertEquals($expectedEndpoint, $p->getValue($rb)); + } + + public function clientUniverseDomainConfigProvider() + { + return [ + [[], 'https://pubsub.googleapis.com/'], // default + [['apiEndpoint' => 'https://foobar.com'], 'https://foobar.com/'], + [['universeDomain' => 'googleapis.com'], 'https://pubsub.googleapis.com/'], + [['universeDomain' => 'abc.def.ghi'], 'https://pubsub.abc.def.ghi/'], + [['universeDomain' => null], '', true], + ]; } /** diff --git a/Storage/tests/Unit/Connection/RestTest.php b/Storage/tests/Unit/Connection/RestTest.php index 42942910abf8..51c97f15e099 100644 --- a/Storage/tests/Unit/Connection/RestTest.php +++ b/Storage/tests/Unit/Connection/RestTest.php @@ -35,6 +35,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; +use UnexpectedValueException; /** * @group storage @@ -60,21 +61,33 @@ public function setUp(): void $this->successBody = '{"canI":"kickIt"}'; } - public function testApiEndpoint() + /** + * @dataProvider clientUniverseDomainConfigProvider + */ + public function testApiEndpointForUniverseDomain($config, $expectedEndpoint, $expectException = false) { - $endpoint = 'https://foobar.com/'; - $rest = TestHelpers::stub(Rest::class, [ - [ - 'apiEndpoint' => $endpoint - ] - ], ['requestBuilder']); + if ($expectException) { + $this->expectException(UnexpectedValueException::class); + } + $rest = TestHelpers::stub(Rest::class, [$config], ['requestBuilder']); $rb = $rest->___getProperty('requestBuilder'); $r = new \ReflectionObject($rb); $p = $r->getProperty('baseUri'); $p->setAccessible(true); - $this->assertEquals($endpoint . 'storage/v1/', $p->getValue($rb)); + $this->assertEquals($expectedEndpoint, $p->getValue($rb)); + } + + public function clientUniverseDomainConfigProvider() + { + return [ + [[], 'https://storage.googleapis.com/storage/v1/'], // default + [['apiEndpoint' => 'https://foobar.com'], 'https://foobar.com/storage/v1/'], + [['universeDomain' => 'googleapis.com'], 'https://storage.googleapis.com/storage/v1/'], + [['universeDomain' => 'abc.def.ghi'], 'https://storage.abc.def.ghi/storage/v1/'], + [['universeDomain' => null], '', true], + ]; } /** From 92b9f114369bfa0cc55431ae588ca730787ddb3e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 4 Jan 2024 10:52:20 -0800 Subject: [PATCH 11/11] bump core version --- BigQuery/composer.json | 2 +- PubSub/composer.json | 2 +- Storage/composer.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BigQuery/composer.json b/BigQuery/composer.json index 6e654b2040f7..9a630ac411ec 100644 --- a/BigQuery/composer.json +++ b/BigQuery/composer.json @@ -5,7 +5,7 @@ "minimum-stability": "stable", "require": { "php": ">=7.4", - "google/cloud-core": "^1.52.7", + "google/cloud-core": "^1.53", "ramsey/uuid": "^3.0|^4.0" }, "require-dev": { diff --git a/PubSub/composer.json b/PubSub/composer.json index 158a6f6545aa..ce441cae5f75 100644 --- a/PubSub/composer.json +++ b/PubSub/composer.json @@ -5,7 +5,7 @@ "minimum-stability": "stable", "require": { "php": ">=7.4", - "google/cloud-core": "^1.52.7", + "google/cloud-core": "^1.53", "google/gax": "^1.26.0" }, "require-dev": { diff --git a/Storage/composer.json b/Storage/composer.json index a5d1195dbe0d..51de9bc16a3c 100644 --- a/Storage/composer.json +++ b/Storage/composer.json @@ -5,7 +5,7 @@ "minimum-stability": "stable", "require": { "php": ">=7.4", - "google/cloud-core": "^1.52.7", + "google/cloud-core": "^1.53", "ramsey/uuid": "^4.2.3" }, "require-dev": {