Skip to content

Commit

Permalink
feat: support universe domain in service account and metadata credent…
Browse files Browse the repository at this point in the history
…ials (#482)
  • Loading branch information
bshaffer authored Dec 14, 2023
1 parent 821d4f3 commit e4aa874
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 11 deletions.
10 changes: 8 additions & 2 deletions src/ApplicationDefaultCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ 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
*
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
Expand All @@ -154,7 +156,8 @@ public static function getCredentials(
array $cacheConfig = null,
CacheItemPoolInterface $cache = null,
$quotaProject = null,
$defaultScope = null
$defaultScope = null,
string $universeDomain = null
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
Expand All @@ -179,6 +182,9 @@ public static function getCredentials(
if ($quotaProject) {
$jsonKey['quota_project_id'] = $quotaProject;
}
if ($universeDomain) {
$jsonKey['universe_domain'] = $universeDomain;
}
$creds = CredentialsLoader::makeCredentials(
$scope,
$jsonKey,
Expand All @@ -187,7 +193,7 @@ public static function getCredentials(
} elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
$creds = new AppIdentityCredentials($anyScope);
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, $anyScope, null, $quotaProject);
$creds = new GCECredentials(null, $anyScope, null, $quotaProject, null, $universeDomain);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}

Expand Down
78 changes: 77 additions & 1 deletion src/Credentials/GCECredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ class GCECredentials extends CredentialsLoader implements
*/
const PROJECT_ID_URI_PATH = 'v1/project/project-id';

/**
* The metadata path of the project ID.
*/
const UNIVERSE_DOMAIN_URI_PATH = 'v1/universe/universe_domain';

/**
* The header whose presence indicates GCE presence.
*/
Expand Down Expand Up @@ -169,6 +174,11 @@ class GCECredentials extends CredentialsLoader implements
*/
private $serviceAccountIdentity;

/**
* @var string
*/
private ?string $universeDomain;

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

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

/**
Expand Down Expand Up @@ -294,6 +308,18 @@ private static function getProjectIdUri()
return $base . self::PROJECT_ID_URI_PATH;
}

/**
* The full uri for accessing the default universe domain.
*
* @return string
*/
private static function getUniverseDomainUri()
{
$base = 'http://' . self::METADATA_IP . '/computeMetadata/';

return $base . self::UNIVERSE_DOMAIN_URI_PATH;
}

/**
* Determines if this an App Engine Flexible instance, by accessing the
* GAE_INSTANCE environment variable.
Expand Down Expand Up @@ -500,6 +526,56 @@ public function getProjectId(callable $httpHandler = null)
return $this->projectId;
}

/**
* Fetch the default universe domain from the metadata server.
*
* Returns null if called outside GCE.
*
* @param callable $httpHandler Callback which delivers psr7 request
* @return string
*/
public function getUniverseDomain(callable $httpHandler = null): string
{
if (null !== $this->universeDomain) {
return $this->universeDomain;
}

$httpHandler = $httpHandler
?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());

if (!$this->hasCheckedOnGce) {
$this->isOnGce = self::onGce($httpHandler);
$this->hasCheckedOnGce = true;
}

if (!$this->isOnGce) {
return self::DEFAULT_UNIVERSE_DOMAIN;
}

try {
$this->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;
}
$this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
}

// 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;
}

return $this->universeDomain;
}

/**
* Fetch the value of a GCE metadata server URI.
*
Expand Down
23 changes: 17 additions & 6 deletions src/Credentials/ServiceAccountCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ class ServiceAccountCredentials extends CredentialsLoader implements
private $jwtAccessCredentials;

/**
* @var string|null
* @var string
*/
private ?string $universeDomain;
private string $universeDomain;

/**
* Create a new ServiceAccountCredentials.
Expand Down Expand Up @@ -164,7 +164,7 @@ public function __construct(
]);

$this->projectId = $jsonKey['project_id'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? self::DEFAULT_UNIVERSE_DOMAIN;
}

/**
Expand Down Expand Up @@ -341,9 +341,6 @@ public function getQuotaProject()
*/
public function getUniverseDomain(): string
{
if (null === $this->universeDomain) {
return self::DEFAULT_UNIVERSE_DOMAIN;
}
return $this->universeDomain;
}

Expand All @@ -355,6 +352,14 @@ private function useSelfSignedJwt()
// When a sub is supplied, the user is using domain-wide delegation, which not available
// with self-signed JWTs
if (null !== $this->auth->getSub()) {
// If we are outside the GDU, we can't use domain-wide delegation
if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
throw new \LogicException(sprintf(
'Service Account subject is configured for the credential. Domain-wide ' .
'delegation is not supported in universes other than %s.',
self::DEFAULT_UNIVERSE_DOMAIN
));
}
return false;
}

Expand All @@ -367,6 +372,12 @@ private function useSelfSignedJwt()
if ($this->useJwtAccessWithScope) {
return true;
}

// If the universe domain is outside the GDU, use JwtAccess for access tokens
if ($this->getUniverseDomain() !== self::DEFAULT_UNIVERSE_DOMAIN) {
return true;
}

return is_null($this->auth->getScope());
}
}
52 changes: 52 additions & 0 deletions tests/ApplicationDefaultCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -804,5 +804,57 @@ public function testUniverseDomainInKeyFile()
putenv(ServiceAccountCredentials::ENV_VAR . '=' . $keyFile);
$creds2 = ApplicationDefaultCredentials::getCredentials();
$this->assertEquals(CredentialsLoader::DEFAULT_UNIVERSE_DOMAIN, $creds2->getUniverseDomain());

// test passing in a different universe domain for "authenticated_user" has no effect.
$creds3 = ApplicationDefaultCredentials::getCredentials(
null,
null,
null,
null,
null,
null,
'example-universe2.com'
);
$this->assertEquals(CredentialsLoader::DEFAULT_UNIVERSE_DOMAIN, $creds3->getUniverseDomain());
}

/** @runInSeparateProcess */
public function testUniverseDomainInGceCredentials()
{
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 overrides metadata server
$creds2 = ApplicationDefaultCredentials::getCredentials(
null, // $scope
$httpHandler = getHandler([
new Response(200, [GCECredentials::FLAVOR_HEADER => 'Google']),
]), // $httpHandler
null, // $cacheConfig
null, // $cache
null, // $quotaProject
null, // $defaultScope
'example-universe2.com' // $universeDomain
);
$this->assertEquals('example-universe2.com', $creds2->getUniverseDomain($httpHandler));

// 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
);
$this->assertEquals(CredentialsLoader::DEFAULT_UNIVERSE_DOMAIN, $creds2->getUniverseDomain($httpHandler));
}
}
57 changes: 55 additions & 2 deletions tests/Credentials/GCECredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,60 @@ public function testGetUniverseDomain()
{
$creds = new GCECredentials();

// Universe domain should always be the default
$this->assertEquals(GCECredentials::DEFAULT_UNIVERSE_DOMAIN, $creds->getUniverseDomain());
// If we are not on GCE, this should return the default
$creds->setIsOnGce(false);
$this->assertEquals(
GCECredentials::DEFAULT_UNIVERSE_DOMAIN,
$creds->getUniverseDomain()
);

// Pretend we are on GCE and mock the http handler.
$expected = 'example-universe.com';
$timesCalled = 0;
$httpHandler = function ($request) use (&$timesCalled, $expected) {
$timesCalled++;
$this->assertEquals(
'/computeMetadata/v1/universe/universe_domain',
$request->getUri()->getPath()
);
$this->assertEquals(1, $timesCalled, 'should only be called once');
return new Psr7\Response(200, [], Utils::streamFor($expected));
};

$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));
}

public function testGetUniverseDomainEmptyStringReturnsDefault()
{
$creds = new GCECredentials();
$creds->setIsOnGce(true);

// Pretend we are on GCE and mock the MDS returning an empty string for the universe domain.
$httpHandler = function ($request) {
$this->assertEquals(
'/computeMetadata/v1/universe/universe_domain',
$request->getUri()->getPath()
);
return new Psr7\Response(200, [], Utils::streamFor(''));
};

// Assert the default universe domain is returned instead of the empty string.
$this->assertEquals(
GCECredentials::DEFAULT_UNIVERSE_DOMAIN,
$creds->getUniverseDomain($httpHandler)
);
}

public function testExplicitUniverseDomain()
{
$expected = 'example-universe.com';
$creds = new GCECredentials(null, null, null, null, null, $expected);
$this->assertEquals($expected, $creds->getUniverseDomain());
}
}
18 changes: 18 additions & 0 deletions tests/Credentials/ServiceAccountCredentialsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,24 @@ public function testSettingBothScopeAndTargetAudienceThrowsException()
);
}

public function testDomainWideDelegationOutsideGduThrowsException()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage(
'Service Account subject is configured for the credential. Domain-wide ' .
'delegation is not supported in universes other than googleapis.com'
);
$testJson = $this->createTestJson() + ['universe_domain' => 'abc.xyz'];
$sub = 'sub123';
$sa = new ServiceAccountCredentials(
null,
$testJson,
$sub
);

$sa->fetchAuthToken();
}

public function testReturnsClientEmail()
{
$testJson = $this->createTestJson();
Expand Down
Loading

0 comments on commit e4aa874

Please sign in to comment.