From 35895ded90b507074b3430a94a5790ddd01f39f0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 23 Apr 2024 17:57:59 -0700 Subject: [PATCH] feat: add universe domain support (#2563) --- composer.json | 4 +-- src/Client.php | 40 +++++++++++++++++++++++++- src/Http/Batch.php | 7 ++++- src/Service.php | 6 +++- src/Service/Resource.php | 16 ++++++----- tests/Google/ClientTest.php | 31 ++++++++++++++++++++ tests/Google/Service/ResourceTest.php | 41 ++++++++++++++++++++++++--- tests/Google/ServiceTest.php | 1 + 8 files changed, 130 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 8cb8b941d..14c5d4207 100644 --- a/composer.json +++ b/composer.json @@ -7,8 +7,8 @@ "license": "Apache-2.0", "require": { "php": "^7.4|^8.0", - "google/auth": "^1.33", - "google/apiclient-services": "~0.200", + "google/auth": "^1.37", + "google/apiclient-services": "~0.350", "firebase/php-jwt": "~6.0", "monolog/monolog": "^2.9||^3.0", "phpseclib/phpseclib": "^3.0.36", diff --git a/src/Client.php b/src/Client.php index 046670551..c7724bd08 100644 --- a/src/Client.php +++ b/src/Client.php @@ -27,6 +27,7 @@ use Google\Auth\Credentials\UserRefreshCredentials; use Google\Auth\CredentialsLoader; use Google\Auth\FetchAuthTokenCache; +use Google\Auth\GetUniverseDomainInterface; use Google\Auth\HttpHandler\HttpHandlerFactory; use Google\Auth\OAuth2; use Google\AuthHandler\AuthHandlerFactory; @@ -131,6 +132,10 @@ class Client * @type string $developer_key * Simple API access key, also from the API console. Ensure you get * a Server key, and not a Browser key. + * **NOTE:** The universe domain is assumed to be "googleapis.com" unless + * explicitly set. When setting an API ley directly via this option, there + * is no way to verify the universe domain. Be sure to set the + * "universe_domain" option if "googleapis.com" is not intended. * @type bool $use_application_default_credentials * For use with Google Cloud Platform * fetch the ApplicationDefaultCredentials, if applicable @@ -164,6 +169,10 @@ class Client * @type bool $api_format_v2 * Setting api_format_v2 will return more detailed error messages * from certain APIs. + * @type string $universe_domain + * Setting the universe domain will change the default rootUrl of the service. + * If not set explicitly, the universe domain will be the value provided in the + *. "GOOGLE_CLOUD_UNIVERSE_DOMAIN" environment variable, or "googleapis.com". * } */ public function __construct(array $config = []) @@ -197,7 +206,9 @@ public function __construct(array $config = []) 'cache_config' => [], 'token_callback' => null, 'jwt' => null, - 'api_format_v2' => false + 'api_format_v2' => false, + 'universe_domain' => getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN') + ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, ], $config); if (!is_null($this->config['credentials'])) { @@ -449,6 +460,7 @@ public function authorize(ClientInterface $http = null) // 3b. If access token exists but is expired, try to refresh it // 4. Check for API Key if ($this->credentials) { + $this->checkUniverseDomain($this->credentials); return $authHandler->attachCredentials( $http, $this->credentials, @@ -458,6 +470,7 @@ public function authorize(ClientInterface $http = null) if ($this->isUsingApplicationDefaultCredentials()) { $credentials = $this->createApplicationDefaultCredentials(); + $this->checkUniverseDomain($credentials); return $authHandler->attachCredentialsCache( $http, $credentials, @@ -473,6 +486,7 @@ public function authorize(ClientInterface $http = null) $scopes, $token['refresh_token'] ); + $this->checkUniverseDomain($credentials); return $authHandler->attachCredentials( $http, $credentials, @@ -525,6 +539,11 @@ public function isUsingApplicationDefaultCredentials() * as calling `clear()` will remove all cache items, including any items not * related to Google API PHP Client.) * + * **NOTE:** The universe domain is assumed to be "googleapis.com" unless + * explicitly set. When setting an access token directly via this method, there + * is no way to verify the universe domain. Be sure to set the "universe_domain" + * option if "googleapis.com" is not intended. + * * @param string|array $token * @throws InvalidArgumentException */ @@ -1318,4 +1337,23 @@ private function createUserRefreshCredentials($scope, $refreshToken) return new UserRefreshCredentials($scope, $creds); } + + private function checkUniverseDomain($credentials) + { + $credentialsUniverse = $credentials instanceof GetUniverseDomainInterface + ? $credentials->getUniverseDomain() + : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; + if ($credentialsUniverse !== $this->getUniverseDomain()) { + throw new DomainException(sprintf( + 'The configured universe domain (%s) does not match the credential universe domain (%s)', + $this->getUniverseDomain(), + $credentialsUniverse + )); + } + } + + public function getUniverseDomain() + { + return $this->config['universe_domain']; + } } diff --git a/src/Http/Batch.php b/src/Http/Batch.php index f37045c01..d16708f20 100644 --- a/src/Http/Batch.php +++ b/src/Http/Batch.php @@ -62,7 +62,12 @@ public function __construct( ) { $this->client = $client; $this->boundary = $boundary ?: mt_rand(); - $this->rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/'); + $rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/'); + $this->rootUrl = str_replace( + 'UNIVERSE_DOMAIN', + $this->client->getUniverseDomain(), + $rootUrl + ); $this->batchPath = $batchPath ?: self::BATCH_PATH; } diff --git a/src/Service.php b/src/Service.php index c97ee9d4f..8c8fe5fa7 100644 --- a/src/Service.php +++ b/src/Service.php @@ -23,7 +23,11 @@ class Service { public $batchPath; + /** + * Only used in getBatch + */ public $rootUrl; + public $rootUrlTemplate; public $version; public $servicePath; public $serviceName; @@ -65,7 +69,7 @@ public function createBatch() return new Batch( $this->client, false, - $this->rootUrl, + $this->rootUrlTemplate ?? $this->rootUrl, $this->batchPath ); } diff --git a/src/Service/Resource.php b/src/Service/Resource.php index ecf402b18..edc3a36ee 100644 --- a/src/Service/Resource.php +++ b/src/Service/Resource.php @@ -45,8 +45,8 @@ class Resource 'prettyPrint' => ['type' => 'string', 'location' => 'query'], ]; - /** @var string $rootUrl */ - private $rootUrl; + /** @var string $rootUrlTemplate */ + private $rootUrlTemplate; /** @var \Google\Client $client */ private $client; @@ -65,7 +65,7 @@ class Resource public function __construct($service, $serviceName, $resourceName, $resource) { - $this->rootUrl = $service->rootUrl; + $this->rootUrlTemplate = $service->rootUrlTemplate ?? $service->rootUrl; $this->client = $service->getClient(); $this->servicePath = $service->servicePath; $this->serviceName = $serviceName; @@ -268,12 +268,14 @@ public function createRequestUri($restPath, $params) $requestUrl = $this->servicePath . $restPath; } - // code for leading slash - if ($this->rootUrl) { - if ('/' !== substr($this->rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) { + if ($this->rootUrlTemplate) { + // code for universe domain + $rootUrl = str_replace('UNIVERSE_DOMAIN', $this->client->getUniverseDomain(), $this->rootUrlTemplate); + // code for leading slash + if ('/' !== substr($rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) { $requestUrl = '/' . $requestUrl; } - $requestUrl = $this->rootUrl . $requestUrl; + $requestUrl = $rootUrl . $requestUrl; } $uriTemplateVars = []; $queryVars = []; diff --git a/tests/Google/ClientTest.php b/tests/Google/ClientTest.php index 94ce8b876..81ea7525e 100644 --- a/tests/Google/ClientTest.php +++ b/tests/Google/ClientTest.php @@ -24,7 +24,9 @@ use Google\Service\Drive; use Google\AuthHandler\AuthHandlerFactory; use Google\Auth\FetchAuthTokenCache; +use Google\Auth\CredentialsLoader; use Google\Auth\GCECache; +use Google\Auth\Credentials\GCECredentials; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; @@ -37,6 +39,7 @@ use ReflectionMethod; use InvalidArgumentException; use Exception; +use DomainException; class ClientTest extends BaseTest { @@ -689,11 +692,20 @@ public function testOnGceCacheAndCacheOptions() $mockCacheItem->get() ->shouldBeCalledTimes(1) ->willReturn(true); + $mockUniverseDomainCacheItem = $this->prophesize(CacheItemInterface::class); + $mockUniverseDomainCacheItem->isHit() + ->willReturn(true); + $mockUniverseDomainCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn('googleapis.com'); $mockCache = $this->prophesize(CacheItemPoolInterface::class); $mockCache->getItem($prefix . GCECache::GCE_CACHE_KEY) ->shouldBeCalledTimes(1) ->willReturn($mockCacheItem->reveal()); + $mockCache->getItem(GCECredentials::cacheKey . 'universe_domain') + ->shouldBeCalledTimes(1) + ->willReturn($mockUniverseDomainCacheItem->reveal()); $client = new Client(['cache_config' => $cacheConfig]); $client->setCache($mockCache->reveal()); @@ -849,6 +861,8 @@ public function testCredentialsOptionWithCredentialsLoader() $credentials = $this->prophesize('Google\Auth\CredentialsLoader'); $credentials->getCacheKey() ->willReturn('cache-key'); + $credentials->getUniverseDomain() + ->willReturn('googleapis.com'); // Ensure the access token provided by our credentials loader is used $credentials->updateMetadata([], null, Argument::any()) @@ -913,4 +927,21 @@ public function testQueryParamsForAuthUrl() ]); $this->assertStringContainsString('&enable_serial_consent=true', $authUrl1); } + public function testUniverseDomainMismatch() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'The configured universe domain (example.com) does not match the credential universe domain (foo.com)' + ); + + $credentials = $this->prophesize(CredentialsLoader::class); + $credentials->getUniverseDomain() + ->shouldBeCalledOnce() + ->willReturn('foo.com'); + $client = new Client([ + 'universe_domain' => 'example.com', + 'credentials' => $credentials->reveal(), + ]); + $client->authorize(); + } } diff --git a/tests/Google/Service/ResourceTest.php b/tests/Google/Service/ResourceTest.php index 17000880a..86e0bef24 100644 --- a/tests/Google/Service/ResourceTest.php +++ b/tests/Google/Service/ResourceTest.php @@ -35,10 +35,11 @@ class TestService extends \Google\Service { - public function __construct(Client $client) + public function __construct(Client $client, $rootUrl = null) { parent::__construct($client); - $this->rootUrl = "https://test.example.com"; + $this->rootUrl = $rootUrl ?: "https://test.example.com"; + $this->rootUrlTemplate = $rootUrl ?: "https://test.UNIVERSE_DOMAIN"; $this->servicePath = ""; $this->version = "v1beta1"; $this->serviceName = "test"; @@ -59,6 +60,7 @@ public function setUp(): void $this->client->getLogger()->willReturn($logger->reveal()); $this->client->shouldDefer()->willReturn(true); $this->client->getHttpClient()->willReturn(new GuzzleClient()); + $this->client->getUniverseDomain()->willReturn('example.com'); $this->service = new TestService($this->client->reveal()); } @@ -106,6 +108,37 @@ public function testCall() $this->assertFalse($request->hasHeader('Content-Type')); } + public function testCallWithUniverseDomainTemplate() + { + $client = $this->prophesize(Client::class); + $logger = $this->prophesize("Monolog\Logger"); + $this->client->getLogger()->willReturn($logger->reveal()); + $this->client->shouldDefer()->willReturn(true); + $this->client->getHttpClient()->willReturn(new GuzzleClient()); + $this->client->getUniverseDomain()->willReturn('example-universe-domain.com'); + + $this->service = new TestService($this->client->reveal()); + + $resource = new GoogleResource( + $this->service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $request = $resource->call("testMethod", [[]]); + $this->assertEquals("https://test.example-universe-domain.com/method/path", (string) $request->getUri()); + $this->assertEquals("POST", $request->getMethod()); + $this->assertFalse($request->hasHeader('Content-Type')); + } + public function testCallWithPostBody() { $resource = new GoogleResource( @@ -130,9 +163,9 @@ public function testCallWithPostBody() public function testCallServiceDefinedRoot() { - $this->service->rootUrl = "https://sample.example.com"; + $service = new TestService($this->client->reveal(), "https://sample.example.com"); $resource = new GoogleResource( - $this->service, + $service, "test", "testResource", [ diff --git a/tests/Google/ServiceTest.php b/tests/Google/ServiceTest.php index 82abeb423..10bb44c7d 100644 --- a/tests/Google/ServiceTest.php +++ b/tests/Google/ServiceTest.php @@ -80,6 +80,7 @@ function ($request) { )->willReturn($response->reveal()); $client->getConfig('base_path')->willReturn(''); + $client->getUniverseDomain()->willReturn(''); $model = new TestService($client->reveal()); $batch = $model->createBatch();