Skip to content

Commit

Permalink
add validation for user-configured universe domain
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Sep 12, 2023
1 parent 961e021 commit dd75d5b
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 32 deletions.
16 changes: 14 additions & 2 deletions src/ApplicationDefaultCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use GuzzleHttp\Client;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
use UnexpectedValueException;

/**
* ApplicationDefaultCredentials obtains the default credentials for
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
49 changes: 34 additions & 15 deletions src/Credentials/GCECredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use UnexpectedValueException;

/**
* GCECredentials supports authorization on Google Compute Engine.
Expand Down Expand Up @@ -179,6 +180,11 @@ class GCECredentials extends CredentialsLoader implements
*/
private ?string $universeDomain;

/**
* @var bool
*/
private ?string $expectedUniverseDomain;

Check failure on line 186 in src/Credentials/GCECredentials.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

PHPDoc tag @var for property Google\Auth\Credentials\GCECredentials::$expectedUniverseDomain with type bool is incompatible with native type string|null.

/**
* @param Iam $iam [optional] An IAM instance.
* @param string|string[] $scope [optional] the scope of the access request,
Expand All @@ -188,16 +194,17 @@ 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,
$scope = null,
$targetAudience = null,
$quotaProject = null,
$serviceAccountIdentity = null,
string $universeDomain = null
string $expectedUniverseDomain = null
) {
$this->iam = $iam;

Expand Down Expand Up @@ -225,7 +232,7 @@ public function __construct(
$this->tokenUri = $tokenUri;
$this->quotaProject = $quotaProject;
$this->serviceAccountIdentity = $serviceAccountIdentity;
$this->universeDomain = $universeDomain;
$this->expectedUniverseDomain = $expectedUniverseDomain;
}

/**
Expand Down Expand Up @@ -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());

Expand All @@ -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;
}

/**
Expand Down
78 changes: 66 additions & 12 deletions tests/ApplicationDefaultCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;
use UnexpectedValueException;

/**
* @runTestsInSeparateProcesses
Expand Down Expand Up @@ -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,
Expand All @@ -808,7 +843,6 @@ public function testUniverseDomainInKeyFile()
null,
'example-universe2.com'
);
$this->assertEquals('example-universe2.com', $creds3->getUniverseDomain());
}

/** @runInSeparateProcess */
Expand All @@ -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));
}
}
29 changes: 26 additions & 3 deletions tests/Credentials/GCECredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use InvalidArgumentException;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use UnexpectedValueException;

/**
* @group credentials
Expand Down Expand Up @@ -537,13 +538,20 @@ public function testGetUniverseDomain()
return new Psr7\Response(200, [], Utils::streamFor($expected));
};

$creds = new GCECredentials();
$creds->setIsOnGce(true);

// Assert correct universe domain.
$this->assertEquals($expected, $creds->getUniverseDomain($httpHandler));

// 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()
Expand All @@ -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));
}
}

0 comments on commit dd75d5b

Please sign in to comment.