From dd75d5b790b4a305ca99c7bb699086113ed9f84e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 12 Sep 2023 16:20:53 -0700 Subject: [PATCH] add validation for user-configured universe domain --- src/ApplicationDefaultCredentials.php | 16 ++++- src/Credentials/GCECredentials.php | 49 +++++++++---- tests/ApplicationDefaultCredentialsTest.php | 78 +++++++++++++++++---- tests/Credentials/GCECredentialsTest.php | 29 +++++++- 4 files changed, 140 insertions(+), 32 deletions(-) diff --git a/src/ApplicationDefaultCredentials.php b/src/ApplicationDefaultCredentials.php index 80437c8c9..741a24552 100644 --- a/src/ApplicationDefaultCredentials.php +++ b/src/ApplicationDefaultCredentials.php @@ -29,6 +29,7 @@ use GuzzleHttp\Client; use InvalidArgumentException; use Psr\Cache\CacheItemPoolInterface; +use UnexpectedValueException; /** * ApplicationDefaultCredentials obtains the default credentials for @@ -144,11 +145,14 @@ public static function getMiddleware( * @param string|string[] $defaultScope The default scope to use if no * user-defined scopes exist, expressed either as an Array or as a * space-delimited string. - * @param string $universeDomain Specifies a universe domain to use for the - * calling client library + * @param string $universeDomain A universe domain to use when none is detected + * from the credentials. If the credentains do contain a universe domain, an + * exception is thrown if it does not match this value. * * @return FetchAuthTokenInterface * @throws DomainException if no implementation can be obtained. + * @throws UnexpectedValueException if the configured universe domain differs from + * the credentials */ public static function getCredentials( $scope = null, @@ -183,6 +187,14 @@ public static function getCredentials( $jsonKey['quota_project_id'] = $quotaProject; } if ($universeDomain) { + if ( + isset($jsonKey['universe_domain']) + && $jsonKey['universe_domain'] != $universeDomain + ) { + throw new UnexpectedValueException( + 'Universe information from credentials is different from configured' + ); + } $jsonKey['universe_domain'] = $universeDomain; } $creds = CredentialsLoader::makeCredentials( diff --git a/src/Credentials/GCECredentials.php b/src/Credentials/GCECredentials.php index 2a6304bb9..47767312d 100644 --- a/src/Credentials/GCECredentials.php +++ b/src/Credentials/GCECredentials.php @@ -31,6 +31,7 @@ use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Psr7\Request; use InvalidArgumentException; +use UnexpectedValueException; /** * GCECredentials supports authorization on Google Compute Engine. @@ -179,6 +180,11 @@ class GCECredentials extends CredentialsLoader implements */ private ?string $universeDomain; + /** + * @var bool + */ + private ?string $expectedUniverseDomain; + /** * @param Iam $iam [optional] An IAM instance. * @param string|string[] $scope [optional] the scope of the access request, @@ -188,8 +194,9 @@ class GCECredentials extends CredentialsLoader implements * charges associated with the request. * @param string $serviceAccountIdentity [optional] Specify a service * account identity name to use instead of "default". - * @param string $universeDomain [optional] Specify a universe domain to use - * instead of fetching one from the metadata server. + * @param string $expectedUniverseDomain [optional] Specify a universe + * domain which will be checked against the one returned by the metadata + * server, and throw an exception on mismatch. */ public function __construct( Iam $iam = null, @@ -197,7 +204,7 @@ public function __construct( $targetAudience = null, $quotaProject = null, $serviceAccountIdentity = null, - string $universeDomain = null + string $expectedUniverseDomain = null ) { $this->iam = $iam; @@ -225,7 +232,7 @@ public function __construct( $this->tokenUri = $tokenUri; $this->quotaProject = $quotaProject; $this->serviceAccountIdentity = $serviceAccountIdentity; - $this->universeDomain = $universeDomain; + $this->expectedUniverseDomain = $expectedUniverseDomain; } /** @@ -533,13 +540,28 @@ public function getProjectId(callable $httpHandler = null) * * @param callable $httpHandler Callback which delivers psr7 request * @return string + * @throws UnexpectedValueException if the detected universe domain differs from + * the expected one. */ public function getUniverseDomain(callable $httpHandler = null): string { - if ($this->universeDomain) { + if (isset($this->universeDomain)) { return $this->universeDomain; } + $universeDomain = $this->detectUniverseDomain($httpHandler); + + if ($this->expectedUniverseDomain && $this->expectedUniverseDomain != $universeDomain) { + throw new UnexpectedValueException( + 'Universe information from credentials is different from configured' + ); + } + + return $this->universeDomain = $universeDomain; + } + + private function detectUniverseDomain(callable $httpHandler = null): string + { $httpHandler = $httpHandler ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient()); @@ -553,27 +575,24 @@ public function getUniverseDomain(callable $httpHandler = null): string } try { - $this->universeDomain = $this->getFromMetadata( - $httpHandler, - self::getUniverseDomainUri() - ); + $universeDomain = $this->getFromMetadata($httpHandler, self::getUniverseDomainUri()); } catch (ClientException $e) { // If the metadata server exists, but returns a 404 for the universe domain, the auth // libraries should safely assume this is an older metadata server running in GCU, and // should return the default universe domain. - if (!$e->hasResponse() || 404 != $e->getResponse()->getStatusCode()) { - throw $e; + if ($e->hasResponse() && 404 == $e->getResponse()->getStatusCode()) { + return self::DEFAULT_UNIVERSE_DOMAIN;; } - $this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN; + throw $e; } // We expect in some cases the metadata server will return an empty string for the universe // domain. In this case, the auth library MUST return the default universe domain. - if ('' === $this->universeDomain) { - $this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN; + if ('' === $universeDomain) { + return self::DEFAULT_UNIVERSE_DOMAIN; } - return $this->universeDomain; + return $universeDomain; } /** diff --git a/tests/ApplicationDefaultCredentialsTest.php b/tests/ApplicationDefaultCredentialsTest.php index 70225fea4..e120b91d1 100644 --- a/tests/ApplicationDefaultCredentialsTest.php +++ b/tests/ApplicationDefaultCredentialsTest.php @@ -31,6 +31,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use ReflectionClass; +use UnexpectedValueException; /** * @runTestsInSeparateProcesses @@ -798,8 +799,42 @@ public function testUniverseDomainInKeyFile() $creds2 = ApplicationDefaultCredentials::getCredentials(); $this->assertEquals('example-universe.com', $creds2->getUniverseDomain()); + } + + /** @runInSeparateProcess */ + public function testUserConfiguredUniverseDomain() + { + // Test no universe domain in keyfile defaults to "googleapis.com" + $keyFile = __DIR__ . '/fixtures/private.json'; + putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); + $creds = ApplicationDefaultCredentials::getCredentials(); + $this->assertEquals(CredentialsLoader::DEFAULT_UNIVERSE_DOMAIN, $creds->getUniverseDomain()); + + // test passing in a different universe domain overrides keyfile + $creds2 = ApplicationDefaultCredentials::getCredentials( + null, + null, + null, + null, + null, + null, + 'example-universe2.com' + ); + $this->assertEquals('example-universe2.com', $creds2->getUniverseDomain()); + } + + /** @runInSeparateProcess */ + public function testPassingInDifferentUniverseDomainFromKeyFileThrowsException() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Universe information from credentials is different from configured'); + + // Test universe domain from keyfile + $keyFile = __DIR__ . '/fixtures2/private.json'; + putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile); + // test passing in a different universe domain overrides keyfile - $creds3 = ApplicationDefaultCredentials::getCredentials( + ApplicationDefaultCredentials::getCredentials( null, null, null, @@ -808,7 +843,6 @@ public function testUniverseDomainInKeyFile() null, 'example-universe2.com' ); - $this->assertEquals('example-universe2.com', $creds3->getUniverseDomain()); } /** @runInSeparateProcess */ @@ -826,28 +860,48 @@ public function testUniverseDomainInGceCredentials() ); $this->assertEquals('example-universe.com', $creds->getUniverseDomain($httpHandler)); - // test passing in a different universe domain overrides metadata server + // test error response returns default universe domain $creds2 = ApplicationDefaultCredentials::getCredentials( null, // $scope $httpHandler = getHandler([ new Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']), + new Response(404), ]), // $httpHandler - null, // $cacheConfig - null, // $cache - null, // $quotaProject - null, // $defaultScope - 'example-universe2.com' // $universeDomain ); - $this->assertEquals('example-universe2.com', $creds2->getUniverseDomain($httpHandler)); + $this->assertEquals(CredentialsLoader::DEFAULT_UNIVERSE_DOMAIN, $creds2->getUniverseDomain($httpHandler)); + } - // test error response returns default universe domain + /** @runInSeparateProcess */ + public function testPassingInDifferentUniverseDomainInGceCredentialsThrowsException() + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Universe information from credentials is different from configured'); + + putenv('HOME'); + + $expectedUniverseDomain = 'example-universe.com'; + $creds = ApplicationDefaultCredentials::getCredentials( + null, // $scope + $httpHandler = getHandler([ + new Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']), + new Response(200, [], Utils::streamFor($expectedUniverseDomain)), + ]) // $httpHandler + ); + $this->assertEquals('example-universe.com', $creds->getUniverseDomain($httpHandler)); + + // test passing in a different universe domain throws exception $creds2 = ApplicationDefaultCredentials::getCredentials( null, // $scope $httpHandler = getHandler([ new Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']), - new Response(404), + new Response(200, [], Utils::streamFor($expectedUniverseDomain)), ]), // $httpHandler + null, // $cacheConfig + null, // $cache + null, // $quotaProject + null, // $defaultScope + 'example-universe2.com' // $universeDomain ); - $this->assertEquals(CredentialsLoader::DEFAULT_UNIVERSE_DOMAIN, $creds2->getUniverseDomain($httpHandler)); + $this->assertEquals('example-universe2.com', $creds2->getUniverseDomain($httpHandler)); } } diff --git a/tests/Credentials/GCECredentialsTest.php b/tests/Credentials/GCECredentialsTest.php index 695ba0195..02d611fcd 100644 --- a/tests/Credentials/GCECredentialsTest.php +++ b/tests/Credentials/GCECredentialsTest.php @@ -28,6 +28,7 @@ use InvalidArgumentException; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use UnexpectedValueException; /** * @group credentials @@ -537,6 +538,7 @@ public function testGetUniverseDomain() return new Psr7\Response(200, [], Utils::streamFor($expected)); }; + $creds = new GCECredentials(); $creds->setIsOnGce(true); // Assert correct universe domain. @@ -544,6 +546,12 @@ public function testGetUniverseDomain() // Assert the result is cached for subsequent calls. $this->assertEquals($expected, $creds->getUniverseDomain($httpHandler)); + + // Assert expected universe domain does not throw exception + $creds2 = new GCECredentials(null, null, null, null, null, $expected); + $creds2->setIsOnGce(true); + $timesCalled = 0; + $this->assertEquals($expected, $creds2->getUniverseDomain($httpHandler)); } public function testGetUniverseDomainEmptyStringReturnsDefault() @@ -567,10 +575,25 @@ public function testGetUniverseDomainEmptyStringReturnsDefault() ); } - public function testExplicitUniverseDomain() + public function testExpectedUniverseDomainThrowsExceptionOnMismatch() { - $expected = 'example-universe.com'; + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Universe information from credentials is different from configured'); + + $expected = 'example-universe1.com'; + $actual = 'example-universe2.com'; + + // Pretend we are on GCE and mock the MDS returning a different universe domain. + $httpHandler = function ($request) use ($actual) { + $this->assertEquals( + '/computeMetadata/v1/universe/universe_domain', + $request->getUri()->getPath() + ); + return new Psr7\Response(200, [], Utils::streamFor($actual)); + }; + $creds = new GCECredentials(null, null, null, null, null, $expected); - $this->assertEquals($expected, $creds->getUniverseDomain()); + $creds->setIsOnGce(true); + $this->assertEquals($expected, $creds->getUniverseDomain($httpHandler)); } }