3434use Google \Auth \UpdateMetadataTrait ;
3535use GuzzleHttp \Psr7 \Request ;
3636use 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}
0 commit comments