Skip to content

Commit 533b387

Browse files
committed
feat: regional access boundaries
1 parent aa37b81 commit 533b387

16 files changed

Lines changed: 1158 additions & 36 deletions

src/ApplicationDefaultCredentials.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public static function getMiddleware(
153153
* @param string|null $universeDomain Specifies a universe domain to use for the
154154
* calling client library.
155155
* @param null|false|LoggerInterface $logger A PSR3 compliant LoggerInterface.
156+
* @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header.
156157
*
157158
* @return FetchAuthTokenInterface
158159
* @throws DomainException if no implementation can be obtained.
@@ -166,6 +167,7 @@ public static function getCredentials(
166167
$defaultScope = null,
167168
?string $universeDomain = null,
168169
null|false|LoggerInterface $logger = null,
170+
bool $enableRegionalAccessBoundary = false
169171
) {
170172
$creds = null;
171173
$jsonKey = CredentialsLoader::fromEnv()
@@ -196,12 +198,18 @@ public static function getCredentials(
196198
$creds = CredentialsLoader::makeCredentials(
197199
$scope,
198200
$jsonKey,
199-
$defaultScope
201+
$defaultScope,
202+
$enableRegionalAccessBoundary
200203
);
201204
} elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
202205
$creds = new AppIdentityCredentials($anyScope);
203206
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
204-
$creds = new GCECredentials(null, $anyScope, null, $quotaProject, null, $universeDomain);
207+
$creds = new GCECredentials(
208+
scope: $anyScope,
209+
quotaProject: $quotaProject,
210+
universeDomain: $universeDomain,
211+
enableRegionalAccessBoundary: $enableRegionalAccessBoundary,
212+
);
205213
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
206214
}
207215

@@ -286,7 +294,7 @@ public static function getIdTokenCredentials(
286294
$targetAudience,
287295
?callable $httpHandler = null,
288296
?array $cacheConfig = null,
289-
?CacheItemPoolInterface $cache = null
297+
?CacheItemPoolInterface $cache = null,
290298
) {
291299
$creds = null;
292300
$jsonKey = CredentialsLoader::fromEnv()
@@ -308,12 +316,20 @@ public static function getIdTokenCredentials(
308316

309317
$creds = match ($jsonKey['type']) {
310318
'authorized_user' => new UserRefreshCredentials(null, $jsonKey, $targetAudience),
311-
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(null, $jsonKey, $targetAudience),
312-
'service_account' => new ServiceAccountCredentials(null, $jsonKey, null, $targetAudience),
319+
'impersonated_service_account' => new ImpersonatedServiceAccountCredentials(
320+
scope: null,
321+
jsonKey: $jsonKey,
322+
targetAudience: $targetAudience,
323+
),
324+
'service_account' => new ServiceAccountCredentials(
325+
scope: null,
326+
jsonKey: $jsonKey,
327+
targetAudience: $targetAudience,
328+
),
313329
default => throw new InvalidArgumentException('invalid value in the type field')
314330
};
315331
} elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
316-
$creds = new GCECredentials(null, null, $targetAudience);
332+
$creds = new GCECredentials(targetAudience: $targetAudience);
317333
$creds->setIsOnGce(true); // save the credentials a trip to the metadata server
318334
}
319335

src/CacheTrait.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ private function getCachedValue($k)
6666
*
6767
* @param mixed $k
6868
* @param mixed $v
69+
* @param int|null $lifetime
6970
* @return mixed
7071
*/
71-
private function setCachedValue($k, $v)
72+
private function setCachedValue($k, $v, ?int $lifetime = null)
7273
{
7374
if (is_null($this->cache)) {
7475
return null;
@@ -81,7 +82,7 @@ private function setCachedValue($k, $v)
8182

8283
$cacheItem = $this->cache->getItem($key);
8384
$cacheItem->set($v);
84-
$cacheItem->expiresAfter($this->cacheConfig['lifetime']);
85+
$cacheItem->expiresAfter($lifetime ?? $this->cacheConfig['lifetime']);
8586
return $this->cache->save($cacheItem);
8687
}
8788

src/Credentials/ExternalAccountCredentials.php

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Google\Auth\UpdateMetadataTrait;
3535
use GuzzleHttp\Psr7\Request;
3636
use InvalidArgumentException;
37+
use LogicException;
3738

3839
/**
3940
* **IMPORTANT**:
@@ -51,7 +52,12 @@ class ExternalAccountCredentials implements
5152
GetUniverseDomainInterface,
5253
ProjectIdProviderInterface
5354
{
54-
use UpdateMetadataTrait;
55+
use UpdateMetadataTrait {
56+
updateMetadata as traitUpdateMetadata;
57+
}
58+
use RegionalAccessBoundaryTrait {
59+
buildRegionalAccessBoundaryLookupUrl as traitBuildRegionalAccessBoundaryLookupUrl;
60+
}
5561

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

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

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

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

229+
private static function getServiceAccountImpersonationEmail(string $serviceAccountImpersonationUrl): string|null
230+
{
231+
// Parse email from URL. The formal looks as follows:
232+
// https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
233+
$regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/';
234+
if (preg_match($regex, $serviceAccountImpersonationUrl, $matches)) {
235+
return $matches['email'];
236+
}
237+
238+
return null;
239+
}
240+
223241
/**
224242
* @param string $stsToken
225243
* @param callable|null $httpHandler
@@ -290,6 +308,37 @@ public function fetchAuthToken(?callable $httpHandler = null, array $headers = [
290308
return $stsToken;
291309
}
292310

311+
/**
312+
* Updates metadata with the authorization token.
313+
*
314+
* @param array<mixed> $metadata metadata hashmap
315+
* @param string $authUri optional auth uri
316+
* @param callable|null $httpHandler callback which delivers psr7 request
317+
* @return array<mixed> updated metadata hashmap
318+
*/
319+
public function updateMetadata(
320+
$metadata,
321+
$authUri = null,
322+
?callable $httpHandler = null
323+
) {
324+
$metadata = $this->traitUpdateMetadata($metadata, $authUri, $httpHandler);
325+
326+
if ($this->enableRegionalAccessBoundary) {
327+
$clientName = $this->serviceAccountImpersonationUrl
328+
? self::getServiceAccountImpersonationEmail($this->serviceAccountImpersonationUrl)
329+
: null;
330+
331+
$metadata = $this->updateRegionalAccessBoundaryMetadata(
332+
$metadata,
333+
$this->buildRegionalAccessBoundaryLookupUrl($clientName),
334+
$this->getUniverseDomain(),
335+
$httpHandler,
336+
);
337+
}
338+
339+
return $metadata;
340+
}
341+
293342
/**
294343
* Get the cache token key for the credentials.
295344
* The cache token key format depends on the type of source
@@ -391,4 +440,36 @@ private function isWorkforcePool(): bool
391440
$regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/#';
392441
return preg_match($regex, $this->auth->getAudience()) === 1;
393442
}
443+
444+
/**
445+
* Builds and returns the URL for the regional access boundary lookup API.
446+
*/
447+
private function buildRegionalAccessBoundaryLookupUrl(string|null $clientName): string
448+
{
449+
if (null !== $clientName) {
450+
return $this->traitBuildRegionalAccessBoundaryLookupUrl(serviceAccountEmail: $clientName);
451+
}
452+
453+
// Try to parse as a workload identity pool.
454+
// Audience format: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
455+
$regex = '/projects\/([^\/]+)\/locations\/global\/workloadIdentityPools\/([^\/]+)/';
456+
if (preg_match($regex, $this->auth->getAudience(), $matches)) {
457+
[$_, $projectNumber, $poolId] = $matches;
458+
459+
return $this->traitBuildRegionalAccessBoundaryLookupUrl(
460+
poolId: $poolId,
461+
projectNumber: $projectNumber,
462+
);
463+
}
464+
465+
// If that fails, try to parse as a workforce pool.
466+
// Audience format: //iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID
467+
if (preg_match('/locations\/[^\/]+\/workforcePools\/([^\/]+)/', $this->auth->getAudience(), $matches)) {
468+
return $this->traitBuildRegionalAccessBoundaryLookupUrl(
469+
poolId: $matches[1],
470+
);
471+
}
472+
473+
throw new LogicException('Invalid audience format');
474+
}
394475
}

src/Credentials/GCECredentials.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class GCECredentials extends CredentialsLoader implements
6666
GetQuotaProjectInterface
6767
{
6868
use IamSignerTrait;
69+
use RegionalAccessBoundaryTrait;
6970

7071
// phpcs:disable
7172
const cacheKey = 'GOOGLE_AUTH_PHP_GCE';
@@ -211,14 +212,16 @@ class GCECredentials extends CredentialsLoader implements
211212
* account identity name to use instead of "default".
212213
* @param string|null $universeDomain [optional] Specify a universe domain to use
213214
* instead of fetching one from the metadata server.
215+
* @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header.
214216
*/
215217
public function __construct(
216218
?Iam $iam = null,
217219
$scope = null,
218220
$targetAudience = null,
219221
$quotaProject = null,
220222
$serviceAccountIdentity = null,
221-
?string $universeDomain = null
223+
?string $universeDomain = null,
224+
bool $enableRegionalAccessBoundary = false
222225
) {
223226
$this->iam = $iam;
224227

@@ -247,6 +250,7 @@ public function __construct(
247250
$this->quotaProject = $quotaProject;
248251
$this->serviceAccountIdentity = $serviceAccountIdentity;
249252
$this->universeDomain = $universeDomain;
253+
$this->enableRegionalAccessBoundary = $enableRegionalAccessBoundary;
250254
}
251255

252256
/**
@@ -631,6 +635,35 @@ public function getUniverseDomain(?callable $httpHandler = null): string
631635
return $this->universeDomain;
632636
}
633637

638+
/**
639+
* Updates metadata with the authorization token.
640+
*
641+
* @param array<mixed> $metadata metadata hashmap
642+
* @param string $authUri optional auth uri
643+
* @param callable|null $httpHandler callback which delivers psr7 request
644+
* @return array<mixed> updated metadata hashmap
645+
*/
646+
public function updateMetadata(
647+
$metadata,
648+
$authUri = null,
649+
?callable $httpHandler = null
650+
) {
651+
$metadata = parent::updateMetadata($metadata, $authUri, $httpHandler);
652+
653+
if ($this->enableRegionalAccessBoundary) {
654+
$metadata = $this->updateRegionalAccessBoundaryMetadata(
655+
$metadata,
656+
$this->buildRegionalAccessBoundaryLookupUrl(
657+
serviceAccountEmail: $this->getClientName($httpHandler)
658+
),
659+
$this->getUniverseDomain($httpHandler),
660+
$httpHandler,
661+
);
662+
}
663+
664+
return $metadata;
665+
}
666+
634667
/**
635668
* Fetch the value of a GCE metadata server URI.
636669
*

src/Credentials/ImpersonatedServiceAccountCredentials.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use Google\Auth\HttpHandler\HttpHandlerFactory;
2727
use Google\Auth\IamSignerTrait;
2828
use Google\Auth\SignBlobInterface;
29+
use Google\Auth\UpdateMetadataInterface;
30+
use Google\Auth\UpdateMetadataTrait;
2931
use GuzzleHttp\Psr7\Request;
3032
use InvalidArgumentException;
3133
use LogicException;
@@ -41,10 +43,13 @@
4143
*/
4244
class ImpersonatedServiceAccountCredentials extends CredentialsLoader implements
4345
SignBlobInterface,
44-
GetUniverseDomainInterface
46+
GetUniverseDomainInterface,
47+
UpdateMetadataInterface
4548
{
4649
use CacheTrait;
4750
use IamSignerTrait;
51+
use UpdateMetadataTrait;
52+
use RegionalAccessBoundaryTrait;
4853

4954
private const CRED_TYPE = 'imp';
5055
private const IAM_SCOPE = 'https://www.googleapis.com/auth/iam';
@@ -100,6 +105,7 @@ public function __construct(
100105
string|array $jsonKey,
101106
private ?string $targetAudience = null,
102107
string|array|null $defaultScope = null,
108+
bool $enableRegionalAccessBoundary = false
103109
) {
104110
if (is_string($jsonKey)) {
105111
if (!file_exists($jsonKey)) {
@@ -140,7 +146,10 @@ public function __construct(
140146
}
141147
$jsonKey['source_credentials'] = match ($jsonKey['source_credentials']['type'] ?? null) {
142148
// Do not pass $defaultScope to ServiceAccountCredentials
143-
'service_account' => new ServiceAccountCredentials($scope, $jsonKey['source_credentials']),
149+
'service_account' => new ServiceAccountCredentials(
150+
scope: $scope,
151+
jsonKey: $jsonKey['source_credentials'],
152+
),
144153
'authorized_user' => new UserRefreshCredentials($scope, $jsonKey['source_credentials']),
145154
'external_account' => new ExternalAccountCredentials($scope, $jsonKey['source_credentials']),
146155
default => throw new \InvalidArgumentException('invalid value in the type field'),
@@ -157,6 +166,7 @@ public function __construct(
157166
);
158167

159168
$this->sourceCredentials = $jsonKey['source_credentials'];
169+
$this->enableRegionalAccessBoundary = $enableRegionalAccessBoundary;
160170
}
161171

162172
/**
@@ -303,4 +313,31 @@ public function getUniverseDomain(): string
303313
? $this->sourceCredentials->getUniverseDomain()
304314
: self::DEFAULT_UNIVERSE_DOMAIN;
305315
}
316+
317+
/**
318+
* Updates metadata with the authorization token.
319+
*
320+
* @param array<mixed> $metadata metadata hashmap
321+
* @param string $authUri optional auth uri
322+
* @param callable|null $httpHandler callback which delivers psr7 request
323+
* @return array<mixed> updated metadata hashmap
324+
*/
325+
public function updateMetadata(
326+
$metadata,
327+
$authUri = null,
328+
?callable $httpHandler = null
329+
) {
330+
$metatadata = parent::updateMetadata($metadata, $authUri, $httpHandler);
331+
332+
$metatadata = $this->updateRegionalAccessBoundaryMetadata(
333+
$metatadata,
334+
$this->buildRegionalAccessBoundaryLookupUrl(
335+
serviceAccountEmail: $this->impersonatedServiceAccountName
336+
),
337+
$this->getUniverseDomain(),
338+
$httpHandler,
339+
);
340+
341+
return $metatadata;
342+
}
306343
}

0 commit comments

Comments
 (0)