Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/ApplicationDefaultCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public static function getMiddleware(
* @param string|null $universeDomain Specifies a universe domain to use for the
* calling client library.
* @param null|false|LoggerInterface $logger A PSR3 compliant LoggerInterface.
* @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header.
*
* @return FetchAuthTokenInterface
* @throws DomainException if no implementation can be obtained.
Expand All @@ -166,6 +167,7 @@ public static function getCredentials(
$defaultScope = null,
?string $universeDomain = null,
null|false|LoggerInterface $logger = null,
bool $enableRegionalAccessBoundary = false
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
Expand Down Expand Up @@ -196,12 +198,18 @@ public static function getCredentials(
$creds = CredentialsLoader::makeCredentials(
$scope,
$jsonKey,
$defaultScope
$defaultScope,
$enableRegionalAccessBoundary
);
} elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
$creds = new AppIdentityCredentials($anyScope);
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, $anyScope, null, $quotaProject, null, $universeDomain);
$creds = new GCECredentials(
scope: $anyScope,
quotaProject: $quotaProject,
universeDomain: $universeDomain,
enableRegionalAccessBoundary: $enableRegionalAccessBoundary,
);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}

Expand Down Expand Up @@ -286,7 +294,7 @@ public static function getIdTokenCredentials(
$targetAudience,
?callable $httpHandler = null,
?array $cacheConfig = null,
?CacheItemPoolInterface $cache = null
?CacheItemPoolInterface $cache = null,
) {
$creds = null;
$jsonKey = CredentialsLoader::fromEnv()
Expand All @@ -308,12 +316,20 @@ public static function getIdTokenCredentials(

$creds = match ($jsonKey['type']) {
'authorized_user' => new UserRefreshCredentials(null, $jsonKey, $targetAudience),
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(null, $jsonKey, $targetAudience),
'service_account' => new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience),
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(
scope: null,
jsonKey: $jsonKey,
targetAudience: $targetAudience,
),
'service_account' => new ServiceAccountCredentials(
scope: null,
jsonKey: $jsonKey,
targetAudience: $targetAudience,
),
default => throw new InvalidArgumentException('invalid value in the type field')
};
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
$creds = new GCECredentials(null, null, $targetAudience);
$creds = new GCECredentials(targetAudience: $targetAudience);
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
}

Expand Down
2 changes: 1 addition & 1 deletion src/Cache/SysVCacheItemPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class SysVCacheItemPool implements CacheItemPoolInterface

const DEFAULT_SEM_PROJ = 'B';

const DEFAULT_MEMSIZE = 10000;
const DEFAULT_MEMSIZE = 100000;

const DEFAULT_PERM = 0600;

Expand Down
5 changes: 3 additions & 2 deletions src/CacheTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ private function getCachedValue($k)
*
* @param mixed $k
* @param mixed $v
* @param int|null $lifetime
* @return mixed
*/
private function setCachedValue($k, $v)
private function setCachedValue($k, $v, ?int $lifetime = null)
{
if (is_null($this->cache)) {
return null;
Expand All @@ -81,7 +82,7 @@ private function setCachedValue($k, $v)

$cacheItem = $this->cache->getItem($key);
$cacheItem->set($v);
$cacheItem->expiresAfter($this->cacheConfig['lifetime']);
$cacheItem->expiresAfter($lifetime ?? $this->cacheConfig['lifetime']);
return $this->cache->save($cacheItem);
}

Expand Down
64 changes: 64 additions & 0 deletions src/Credentials/ExternalAccountAuthorizedUserCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\GetUniverseDomainInterface;
use Google\Auth\OAuth2;
use Google\Auth\UpdateMetadataTrait;
use InvalidArgumentException;
use LogicException;

/**
* Authenticates requests using External Account Authorized User credentials.
Expand All @@ -32,6 +34,13 @@
*/
class ExternalAccountAuthorizedUserCredentials extends CredentialsLoader implements GetQuotaProjectInterface
{
use RegionalAccessBoundaryTrait {
buildRegionalAccessBoundaryLookupUrl as traitBuildRegionalAccessBoundaryLookupUrl;
}
use UpdateMetadataTrait {
updateMetadata as traitUpdateMetadata;
}

/**
* Used in observability metric headers
*
Expand Down Expand Up @@ -123,6 +132,33 @@ public function fetchAuthToken(?callable $httpHandler = null, array $headers = [
);
}

/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
$metadata = $this->traitUpdateMetadata($metadata, $authUri, $httpHandler);

if ($this->enableRegionalAccessBoundary) {
$metadata = $this->updateRegionalAccessBoundaryMetadata(
$metadata,
$this->buildRegionalAccessBoundaryLookupUrl(),
$this->getUniverseDomain(),
$httpHandler,
);
}

return $metadata;
}

/**
* Return the Cache Key for the credentials.
* The format for the Cache key is
Expand Down Expand Up @@ -181,4 +217,32 @@ protected function getCredType(): string
{
return self::CRED_TYPE;
}

/**
* Builds and returns the URL for the RAB lookup API.
*/
private function buildRegionalAccessBoundaryLookupUrl(): string
{
// Try to parse as a workload identity pool.
// Audience format: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
$regex = '/projects\/([^\/]+)\/locations\/global\/workloadIdentityPools\/([^\/]+)/';
if (preg_match($regex, $this->auth->getAudience(), $matches)) {
[$_, $projectNumber, $poolId] = $matches;

return $this->traitBuildRegionalAccessBoundaryLookupUrl(
poolId: $poolId,
projectNumber: $projectNumber,
);
}

// If that fails, try to parse as a workforce pool.
// Audience format: //iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID
if (preg_match('/locations\/[^\/]+\/workforcePools\/([^\/]+)/', $this->auth->getAudience(), $matches)) {
return $this->traitBuildRegionalAccessBoundaryLookupUrl(
poolId: $matches[1],
);
}

throw new LogicException('Invalid audience format');
}
}
95 changes: 88 additions & 7 deletions src/Credentials/ExternalAccountCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use Google\Auth\UpdateMetadataTrait;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use LogicException;

/**
* **IMPORTANT**:
Expand All @@ -51,7 +52,12 @@ class ExternalAccountCredentials implements
GetUniverseDomainInterface,
ProjectIdProviderInterface
{
use UpdateMetadataTrait;
use UpdateMetadataTrait {
updateMetadata as traitUpdateMetadata;
}
use RegionalAccessBoundaryTrait {
buildRegionalAccessBoundaryLookupUrl as traitBuildRegionalAccessBoundaryLookupUrl;
}

private const EXTERNAL_ACCOUNT_TYPE = 'external_account';
private const CLOUD_RESOURCE_MANAGER_URL = 'https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s';
Expand All @@ -69,10 +75,12 @@ class ExternalAccountCredentials implements
* @param string|string[] $scope The scope of the access request, expressed either as an array
* or as a space-delimited string.
* @param array<mixed> $jsonKey JSON credentials as an associative array.
* @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header.
*/
public function __construct(
$scope,
array $jsonKey
array $jsonKey,
bool $enableRegionalAccessBoundary = false
) {
if (!array_key_exists('type', $jsonKey)) {
throw new InvalidArgumentException('json key is missing the type field');
Expand Down Expand Up @@ -114,6 +122,7 @@ public function __construct(
$this->quotaProject = $jsonKey['quota_project_id'] ?? null;
$this->workforcePoolUserProject = $jsonKey['workforce_pool_user_project'] ?? null;
$this->universeDomain = $jsonKey['universe_domain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN;
$this->enableRegionalAccessBoundary = $enableRegionalAccessBoundary;

$this->auth = new OAuth2([
'tokenCredentialUri' => $jsonKey['token_url'],
Expand Down Expand Up @@ -200,11 +209,8 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr
}

if ($serviceAccountImpersonationUrl = $jsonKey['service_account_impersonation_url'] ?? null) {
// Parse email from URL. The formal looks as follows:
// https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
$env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $matches['email'];
if ($email = self::getServiceAccountImpersonationEmail($serviceAccountImpersonationUrl)) {
$env['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL'] = $email;
}
}

Expand All @@ -220,6 +226,18 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr
throw new InvalidArgumentException('Unable to determine credential source from json key.');
}

private static function getServiceAccountImpersonationEmail(string $serviceAccountImpersonationUrl): string|null
{
// Parse email from URL. The formal looks as follows:
// https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
return $matches['email'];
}

return null;
}

/**
* @param string $stsToken
* @param callable|null $httpHandler
Expand Down Expand Up @@ -290,6 +308,37 @@ public function fetchAuthToken(?callable $httpHandler = null, array $headers = [
return $stsToken;
}

/**
* Updates metadata with the authorization token.
*
* @param array<mixed> $metadata metadata hashmap
* @param string $authUri optional auth uri
* @param callable|null $httpHandler callback which delivers psr7 request
* @return array<mixed> updated metadata hashmap
*/
public function updateMetadata(
$metadata,
$authUri = null,
?callable $httpHandler = null
) {
$metadata = $this->traitUpdateMetadata($metadata, $authUri, $httpHandler);

if ($this->enableRegionalAccessBoundary) {
$clientName = $this->serviceAccountImpersonationUrl
? self::getServiceAccountImpersonationEmail($this->serviceAccountImpersonationUrl)
: null;

$metadata = $this->updateRegionalAccessBoundaryMetadata(
$metadata,
$this->buildRegionalAccessBoundaryLookupUrl($clientName),
$this->getUniverseDomain(),
$httpHandler,
);
}

return $metadata;
}

/**
* Get the cache token key for the credentials.
* The cache token key format depends on the type of source
Expand Down Expand Up @@ -391,4 +440,36 @@ private function isWorkforcePool(): bool
$regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
return preg_match($regex, $this->auth->getAudience()) === 1;
}

/**
* Builds and returns the URL for the regional access boundary lookup API.
*/
private function buildRegionalAccessBoundaryLookupUrl(string|null $clientName): string
{
if (null !== $clientName) {
return $this->traitBuildRegionalAccessBoundaryLookupUrl(serviceAccountEmail: $clientName);
}

// Try to parse as a workload identity pool.
// Audience format: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
$regex = '/projects\/([^\/]+)\/locations\/global\/workloadIdentityPools\/([^\/]+)/';
if (preg_match($regex, $this->auth->getAudience(), $matches)) {
[$_, $projectNumber, $poolId] = $matches;

return $this->traitBuildRegionalAccessBoundaryLookupUrl(
poolId: $poolId,
projectNumber: $projectNumber,
);
}

// If that fails, try to parse as a workforce pool.
// Audience format: //iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID
if (preg_match('/locations\/[^\/]+\/workforcePools\/([^\/]+)/', $this->auth->getAudience(), $matches)) {
return $this->traitBuildRegionalAccessBoundaryLookupUrl(
poolId: $matches[1],
);
}

throw new LogicException('Invalid audience format');
}
}
Loading
Loading