3030use Google \Auth \HttpHandler \HttpHandlerFactory ;
3131use Google \Auth \OAuth2 ;
3232use Google \Auth \ProjectIdProviderInterface ;
33+ use Google \Auth \TrustBoundaryTrait ;
3334use Google \Auth \UpdateMetadataInterface ;
3435use Google \Auth \UpdateMetadataTrait ;
3536use GuzzleHttp \Psr7 \Request ;
3637use InvalidArgumentException ;
38+ use LogicException ;
3739
3840/**
3941 * **IMPORTANT**:
@@ -51,7 +53,12 @@ class ExternalAccountCredentials implements
5153 GetUniverseDomainInterface,
5254 ProjectIdProviderInterface
5355{
54- use UpdateMetadataTrait;
56+ use UpdateMetadataTrait {
57+ updateMetadata as traitUpdateMetadata;
58+ }
59+ use TrustBoundaryTrait {
60+ buildTrustBoundaryLookupUrl as traitBuildTrustBoundaryLookupUrl;
61+ }
5562
5663 private const EXTERNAL_ACCOUNT_TYPE = 'external_account ' ;
5764 private const CLOUD_RESOURCE_MANAGER_URL = 'https://cloudresourcemanager.UNIVERSE_DOMAIN/v1/projects/%s ' ;
@@ -69,10 +76,12 @@ class ExternalAccountCredentials implements
6976 * @param string|string[] $scope The scope of the access request, expressed either as an array
7077 * or as a space-delimited string.
7178 * @param array<mixed> $jsonKey JSON credentials as an associative array.
79+ * @param bool $enableTrustBoundary Lookup and include the trust boundary header.
7280 */
7381 public function __construct (
7482 $ scope ,
75- array $ jsonKey
83+ array $ jsonKey ,
84+ bool $ enableTrustBoundary = false
7685 ) {
7786 if (!array_key_exists ('type ' , $ jsonKey )) {
7887 throw new InvalidArgumentException ('json key is missing the type field ' );
@@ -114,6 +123,7 @@ public function __construct(
114123 $ this ->quotaProject = $ jsonKey ['quota_project_id ' ] ?? null ;
115124 $ this ->workforcePoolUserProject = $ jsonKey ['workforce_pool_user_project ' ] ?? null ;
116125 $ this ->universeDomain = $ jsonKey ['universe_domain ' ] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN ;
126+ $ this ->enableTrustBoundary = $ enableTrustBoundary ;
117127
118128 $ this ->auth = new OAuth2 ([
119129 'tokenCredentialUri ' => $ jsonKey ['token_url ' ],
@@ -200,11 +210,8 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr
200210 }
201211
202212 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 ' ];
213+ if ($ email = self ::getServiceAccountImpersonationEmail ($ serviceAccountImpersonationUrl )) {
214+ $ env ['GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL ' ] = $ email ;
208215 }
209216 }
210217
@@ -220,6 +227,18 @@ private static function buildCredentialSource(array $jsonKey): ExternalAccountCr
220227 throw new InvalidArgumentException ('Unable to determine credential source from json key. ' );
221228 }
222229
230+ private static function getServiceAccountImpersonationEmail (string $ serviceAccountImpersonationUrl ): string |null
231+ {
232+ // Parse email from URL. The formal looks as follows:
233+ // https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
234+ $ regex = '/serviceAccounts\/(?<email>[^:]+):generateAccessToken$/ ' ;
235+ if (preg_match ($ regex , $ serviceAccountImpersonationUrl , $ matches )) {
236+ return $ matches ['email ' ];
237+ }
238+
239+ return null ;
240+ }
241+
223242 /**
224243 * @param string $stsToken
225244 * @param callable|null $httpHandler
@@ -290,6 +309,37 @@ public function fetchAuthToken(?callable $httpHandler = null, array $headers = [
290309 return $ stsToken ;
291310 }
292311
312+ /**
313+ * Updates metadata with the authorization token.
314+ *
315+ * @param array<mixed> $metadata metadata hashmap
316+ * @param string $authUri optional auth uri
317+ * @param callable|null $httpHandler callback which delivers psr7 request
318+ * @return array<mixed> updated metadata hashmap
319+ */
320+ public function updateMetadata (
321+ $ metadata ,
322+ $ authUri = null ,
323+ ?callable $ httpHandler = null
324+ ) {
325+ $ metadata = $ this ->traitUpdateMetadata ($ metadata , $ authUri , $ httpHandler );
326+
327+ if ($ this ->enableTrustBoundary ) {
328+ $ clientName = $ this ->serviceAccountImpersonationUrl
329+ ? self ::getServiceAccountImpersonationEmail ($ this ->serviceAccountImpersonationUrl )
330+ : '' ; // @TODO: What do we do when this is empty?
331+
332+ $ metadata = $ this ->updateTrustBoundaryMetadata (
333+ $ metadata ,
334+ $ this ->buildTrustBoundaryLookupUrl (),
335+ $ this ->getUniverseDomain ($ httpHandler ),
336+ $ httpHandler ,
337+ );
338+ }
339+
340+ return $ metadata ;
341+ }
342+
293343 /**
294344 * Get the cache token key for the credentials.
295345 * The cache token key format depends on the type of source
@@ -391,4 +441,32 @@ private function isWorkforcePool(): bool
391441 $ regex = '#//iam\.googleapis\.com/locations/[^/]+/workforcePools/# ' ;
392442 return preg_match ($ regex , $ this ->auth ->getAudience ()) === 1 ;
393443 }
444+
445+ /**
446+ * Builds and returns the URL for the trust boundary lookup API.
447+ */
448+ private function buildTrustBoundaryLookupUrl (): string
449+ {
450+ // Try to parse as a workload identity pool.
451+ // Audience format: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
452+ $ regex = '/projects\/([^\/]+)\/locations\/global\/workloadIdentityPools\/([^\/]+)/ ' ;
453+ if (preg_match ($ regex , $ this ->auth ->getAudience (), $ matches )) {
454+ [$ _ , $ projectNumber , $ poolId ] = $ matches ;
455+
456+ return $ this ->traitBuildTrustBoundaryLookupUrl (
457+ poolId: $ poolId ,
458+ projectNumber: $ projectNumber ,
459+ );
460+ }
461+
462+ // If that fails, try to parse as a workforce pool.
463+ // Audience format: //iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID
464+ if (preg_match ('/locations\/[^\/]+\/workforcePools\/([^\/]+)/ ' , $ this ->auth ->getAudience (), $ matches )) {
465+ return $ this ->traitBuildTrustBoundaryLookupUrl (
466+ poolId: $ matches [1 ],
467+ );
468+ }
469+
470+ throw new LogicException ("Invalid audience format. " );
471+ }
394472}
0 commit comments