source: vendor/google/auth/src/Credentials/GCECredentials.php@ e3d4e0a

Last change on this file since e3d4e0a was e3d4e0a, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload project files

  • Property mode set to 100644
File size: 20.3 KB
Line 
1<?php
2/*
3 * Copyright 2015 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Auth\Credentials;
19
20use COM;
21use com_exception;
22use Google\Auth\CredentialsLoader;
23use Google\Auth\GetQuotaProjectInterface;
24use Google\Auth\HttpHandler\HttpClientCache;
25use Google\Auth\HttpHandler\HttpHandlerFactory;
26use Google\Auth\Iam;
27use Google\Auth\IamSignerTrait;
28use Google\Auth\ProjectIdProviderInterface;
29use Google\Auth\SignBlobInterface;
30use GuzzleHttp\Exception\ClientException;
31use GuzzleHttp\Exception\ConnectException;
32use GuzzleHttp\Exception\RequestException;
33use GuzzleHttp\Exception\ServerException;
34use GuzzleHttp\Psr7\Request;
35use InvalidArgumentException;
36
37/**
38 * GCECredentials supports authorization on Google Compute Engine.
39 *
40 * It can be used to authorize requests using the AuthTokenMiddleware, but will
41 * only succeed if being run on GCE:
42 *
43 * use Google\Auth\Credentials\GCECredentials;
44 * use Google\Auth\Middleware\AuthTokenMiddleware;
45 * use GuzzleHttp\Client;
46 * use GuzzleHttp\HandlerStack;
47 *
48 * $gce = new GCECredentials();
49 * $middleware = new AuthTokenMiddleware($gce);
50 * $stack = HandlerStack::create();
51 * $stack->push($middleware);
52 *
53 * $client = new Client([
54 * 'handler' => $stack,
55 * 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
56 * 'auth' => 'google_auth'
57 * ]);
58 *
59 * $res = $client->get('myproject/taskqueues/myqueue');
60 */
61class GCECredentials extends CredentialsLoader implements
62 SignBlobInterface,
63 ProjectIdProviderInterface,
64 GetQuotaProjectInterface
65{
66 use IamSignerTrait;
67
68 // phpcs:disable
69 const cacheKey = 'GOOGLE_AUTH_PHP_GCE';
70 // phpcs:enable
71
72 /**
73 * The metadata IP address on appengine instances.
74 *
75 * The IP is used instead of the domain 'metadata' to avoid slow responses
76 * when not on Compute Engine.
77 */
78 const METADATA_IP = '169.254.169.254';
79
80 /**
81 * The metadata path of the default token.
82 */
83 const TOKEN_URI_PATH = 'v1/instance/service-accounts/default/token';
84
85 /**
86 * The metadata path of the default id token.
87 */
88 const ID_TOKEN_URI_PATH = 'v1/instance/service-accounts/default/identity';
89
90 /**
91 * The metadata path of the client ID.
92 */
93 const CLIENT_ID_URI_PATH = 'v1/instance/service-accounts/default/email';
94
95 /**
96 * The metadata path of the project ID.
97 */
98 const PROJECT_ID_URI_PATH = 'v1/project/project-id';
99
100 /**
101 * The metadata path of the project ID.
102 */
103 const UNIVERSE_DOMAIN_URI_PATH = 'v1/universe/universe-domain';
104
105 /**
106 * The header whose presence indicates GCE presence.
107 */
108 const FLAVOR_HEADER = 'Metadata-Flavor';
109
110 /**
111 * The Linux file which contains the product name.
112 */
113 private const GKE_PRODUCT_NAME_FILE = '/sys/class/dmi/id/product_name';
114
115 /**
116 * The Windows Registry key path to the product name
117 */
118 private const WINDOWS_REGISTRY_KEY_PATH = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';
119
120 /**
121 * The Windows registry key name for the product name
122 */
123 private const WINDOWS_REGISTRY_KEY_NAME = 'SystemProductName';
124
125 /**
126 * The Name of the product expected from the windows registry
127 */
128 private const PRODUCT_NAME = 'Google';
129
130 private const CRED_TYPE = 'mds';
131
132 /**
133 * Note: the explicit `timeout` and `tries` below is a workaround. The underlying
134 * issue is that resolving an unknown host on some networks will take
135 * 20-30 seconds; making this timeout short fixes the issue, but
136 * could lead to false negatives in the event that we are on GCE, but
137 * the metadata resolution was particularly slow. The latter case is
138 * "unlikely" since the expected 4-nines time is about 0.5 seconds.
139 * This allows us to limit the total ping maximum timeout to 1.5 seconds
140 * for developer desktop scenarios.
141 */
142 const MAX_COMPUTE_PING_TRIES = 3;
143 const COMPUTE_PING_CONNECTION_TIMEOUT_S = 0.5;
144
145 /**
146 * Flag used to ensure that the onGCE test is only done once;.
147 *
148 * @var bool
149 */
150 private $hasCheckedOnGce = false;
151
152 /**
153 * Flag that stores the value of the onGCE check.
154 *
155 * @var bool
156 */
157 private $isOnGce = false;
158
159 /**
160 * Result of fetchAuthToken.
161 *
162 * @var array<mixed>
163 */
164 protected $lastReceivedToken;
165
166 /**
167 * @var string|null
168 */
169 private $clientName;
170
171 /**
172 * @var string|null
173 */
174 private $projectId;
175
176 /**
177 * @var string
178 */
179 private $tokenUri;
180
181 /**
182 * @var string
183 */
184 private $targetAudience;
185
186 /**
187 * @var string|null
188 */
189 private $quotaProject;
190
191 /**
192 * @var string|null
193 */
194 private $serviceAccountIdentity;
195
196 /**
197 * @var string
198 */
199 private ?string $universeDomain;
200
201 /**
202 * @param Iam|null $iam [optional] An IAM instance.
203 * @param string|string[] $scope [optional] the scope of the access request,
204 * expressed either as an array or as a space-delimited string.
205 * @param string $targetAudience [optional] The audience for the ID token.
206 * @param string $quotaProject [optional] Specifies a project to bill for access
207 * charges associated with the request.
208 * @param string $serviceAccountIdentity [optional] Specify a service
209 * account identity name to use instead of "default".
210 * @param string|null $universeDomain [optional] Specify a universe domain to use
211 * instead of fetching one from the metadata server.
212 */
213 public function __construct(
214 ?Iam $iam = null,
215 $scope = null,
216 $targetAudience = null,
217 $quotaProject = null,
218 $serviceAccountIdentity = null,
219 ?string $universeDomain = null
220 ) {
221 $this->iam = $iam;
222
223 if ($scope && $targetAudience) {
224 throw new InvalidArgumentException(
225 'Scope and targetAudience cannot both be supplied'
226 );
227 }
228
229 $tokenUri = self::getTokenUri($serviceAccountIdentity);
230 if ($scope) {
231 if (is_string($scope)) {
232 $scope = explode(' ', $scope);
233 }
234
235 $scope = implode(',', $scope);
236
237 $tokenUri = $tokenUri . '?scopes=' . $scope;
238 } elseif ($targetAudience) {
239 $tokenUri = self::getIdTokenUri($serviceAccountIdentity);
240 $tokenUri = $tokenUri . '?audience=' . $targetAudience;
241 $this->targetAudience = $targetAudience;
242 }
243
244 $this->tokenUri = $tokenUri;
245 $this->quotaProject = $quotaProject;
246 $this->serviceAccountIdentity = $serviceAccountIdentity;
247 $this->universeDomain = $universeDomain;
248 }
249
250 /**
251 * The full uri for accessing the default token.
252 *
253 * @param string $serviceAccountIdentity [optional] Specify a service
254 * account identity name to use instead of "default".
255 * @return string
256 */
257 public static function getTokenUri($serviceAccountIdentity = null)
258 {
259 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
260 $base .= self::TOKEN_URI_PATH;
261
262 if ($serviceAccountIdentity) {
263 return str_replace(
264 '/default/',
265 '/' . $serviceAccountIdentity . '/',
266 $base
267 );
268 }
269 return $base;
270 }
271
272 /**
273 * The full uri for accessing the default service account.
274 *
275 * @param string $serviceAccountIdentity [optional] Specify a service
276 * account identity name to use instead of "default".
277 * @return string
278 */
279 public static function getClientNameUri($serviceAccountIdentity = null)
280 {
281 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
282 $base .= self::CLIENT_ID_URI_PATH;
283
284 if ($serviceAccountIdentity) {
285 return str_replace(
286 '/default/',
287 '/' . $serviceAccountIdentity . '/',
288 $base
289 );
290 }
291
292 return $base;
293 }
294
295 /**
296 * The full uri for accesesing the default identity token.
297 *
298 * @param string $serviceAccountIdentity [optional] Specify a service
299 * account identity name to use instead of "default".
300 * @return string
301 */
302 private static function getIdTokenUri($serviceAccountIdentity = null)
303 {
304 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
305 $base .= self::ID_TOKEN_URI_PATH;
306
307 if ($serviceAccountIdentity) {
308 return str_replace(
309 '/default/',
310 '/' . $serviceAccountIdentity . '/',
311 $base
312 );
313 }
314
315 return $base;
316 }
317
318 /**
319 * The full uri for accessing the default project ID.
320 *
321 * @return string
322 */
323 private static function getProjectIdUri()
324 {
325 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
326
327 return $base . self::PROJECT_ID_URI_PATH;
328 }
329
330 /**
331 * The full uri for accessing the default universe domain.
332 *
333 * @return string
334 */
335 private static function getUniverseDomainUri()
336 {
337 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
338
339 return $base . self::UNIVERSE_DOMAIN_URI_PATH;
340 }
341
342 /**
343 * Determines if this an App Engine Flexible instance, by accessing the
344 * GAE_INSTANCE environment variable.
345 *
346 * @return bool true if this an App Engine Flexible Instance, false otherwise
347 */
348 public static function onAppEngineFlexible()
349 {
350 return substr((string) getenv('GAE_INSTANCE'), 0, 4) === 'aef-';
351 }
352
353 /**
354 * Determines if this a GCE instance, by accessing the expected metadata
355 * host.
356 * If $httpHandler is not specified a the default HttpHandler is used.
357 *
358 * @param callable|null $httpHandler callback which delivers psr7 request
359 * @return bool True if this a GCEInstance, false otherwise
360 */
361 public static function onGce(?callable $httpHandler = null)
362 {
363 $httpHandler = $httpHandler
364 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
365
366 $checkUri = 'http://' . self::METADATA_IP;
367 for ($i = 1; $i <= self::MAX_COMPUTE_PING_TRIES; $i++) {
368 try {
369 // Comment from: oauth2client/client.py
370 //
371 // Note: the explicit `timeout` below is a workaround. The underlying
372 // issue is that resolving an unknown host on some networks will take
373 // 20-30 seconds; making this timeout short fixes the issue, but
374 // could lead to false negatives in the event that we are on GCE, but
375 // the metadata resolution was particularly slow. The latter case is
376 // "unlikely".
377 $resp = $httpHandler(
378 new Request(
379 'GET',
380 $checkUri,
381 [
382 self::FLAVOR_HEADER => 'Google',
383 self::$metricMetadataKey => self::getMetricsHeader('', 'mds')
384 ]
385 ),
386 ['timeout' => self::COMPUTE_PING_CONNECTION_TIMEOUT_S]
387 );
388
389 return $resp->getHeaderLine(self::FLAVOR_HEADER) == 'Google';
390 } catch (ClientException $e) {
391 } catch (ServerException $e) {
392 } catch (RequestException $e) {
393 } catch (ConnectException $e) {
394 }
395 }
396
397 if (PHP_OS === 'Windows' || PHP_OS === 'WINNT') {
398 return self::detectResidencyWindows(
399 self::WINDOWS_REGISTRY_KEY_PATH . self::WINDOWS_REGISTRY_KEY_NAME
400 );
401 }
402
403 // Detect GCE residency on Linux
404 return self::detectResidencyLinux(self::GKE_PRODUCT_NAME_FILE);
405 }
406
407 private static function detectResidencyLinux(string $productNameFile): bool
408 {
409 if (file_exists($productNameFile)) {
410 $productName = trim((string) file_get_contents($productNameFile));
411 return 0 === strpos($productName, self::PRODUCT_NAME);
412 }
413 return false;
414 }
415
416 private static function detectResidencyWindows(string $registryProductKey): bool
417 {
418 if (!class_exists(COM::class)) {
419 // the COM extension must be installed and enabled to detect Windows residency
420 // see https://www.php.net/manual/en/book.com.php
421 return false;
422 }
423
424 $shell = new COM('WScript.Shell');
425 $productName = null;
426
427 try {
428 $productName = $shell->regRead($registryProductKey);
429 } catch (com_exception) {
430 // This means that we tried to read a key that doesn't exist on the registry
431 // which might mean that it is a windows instance that is not on GCE
432 return false;
433 }
434
435 return 0 === strpos($productName, self::PRODUCT_NAME);
436 }
437
438 /**
439 * Implements FetchAuthTokenInterface#fetchAuthToken.
440 *
441 * Fetches the auth tokens from the GCE metadata host if it is available.
442 * If $httpHandler is not specified a the default HttpHandler is used.
443 *
444 * @param callable|null $httpHandler callback which delivers psr7 request
445 *
446 * @return array<mixed> {
447 * A set of auth related metadata, based on the token type.
448 *
449 * @type string $access_token for access tokens
450 * @type int $expires_in for access tokens
451 * @type string $token_type for access tokens
452 * @type string $id_token for ID tokens
453 * }
454 * @throws \Exception
455 */
456 public function fetchAuthToken(?callable $httpHandler = null)
457 {
458 $httpHandler = $httpHandler
459 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
460
461 if (!$this->hasCheckedOnGce) {
462 $this->isOnGce = self::onGce($httpHandler);
463 $this->hasCheckedOnGce = true;
464 }
465 if (!$this->isOnGce) {
466 return []; // return an empty array with no access token
467 }
468
469 $response = $this->getFromMetadata(
470 $httpHandler,
471 $this->tokenUri,
472 $this->applyTokenEndpointMetrics([], $this->targetAudience ? 'it' : 'at')
473 );
474
475 if ($this->targetAudience) {
476 return $this->lastReceivedToken = ['id_token' => $response];
477 }
478
479 if (null === $json = json_decode($response, true)) {
480 throw new \Exception('Invalid JSON response');
481 }
482
483 $json['expires_at'] = time() + $json['expires_in'];
484
485 // store this so we can retrieve it later
486 $this->lastReceivedToken = $json;
487
488 return $json;
489 }
490
491 /**
492 * Returns the Cache Key for the credential token.
493 * The format for the cache key is:
494 * TokenURI
495 *
496 * @return string
497 */
498 public function getCacheKey()
499 {
500 return $this->tokenUri;
501 }
502
503 /**
504 * @return array<mixed>|null
505 */
506 public function getLastReceivedToken()
507 {
508 if ($this->lastReceivedToken) {
509 if (array_key_exists('id_token', $this->lastReceivedToken)) {
510 return $this->lastReceivedToken;
511 }
512
513 return [
514 'access_token' => $this->lastReceivedToken['access_token'],
515 'expires_at' => $this->lastReceivedToken['expires_at']
516 ];
517 }
518
519 return null;
520 }
521
522 /**
523 * Get the client name from GCE metadata.
524 *
525 * Subsequent calls will return a cached value.
526 *
527 * @param callable|null $httpHandler callback which delivers psr7 request
528 * @return string
529 */
530 public function getClientName(?callable $httpHandler = null)
531 {
532 if ($this->clientName) {
533 return $this->clientName;
534 }
535
536 $httpHandler = $httpHandler
537 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
538
539 if (!$this->hasCheckedOnGce) {
540 $this->isOnGce = self::onGce($httpHandler);
541 $this->hasCheckedOnGce = true;
542 }
543
544 if (!$this->isOnGce) {
545 return '';
546 }
547
548 $this->clientName = $this->getFromMetadata(
549 $httpHandler,
550 self::getClientNameUri($this->serviceAccountIdentity)
551 );
552
553 return $this->clientName;
554 }
555
556 /**
557 * Fetch the default Project ID from compute engine.
558 *
559 * Returns null if called outside GCE.
560 *
561 * @param callable|null $httpHandler Callback which delivers psr7 request
562 * @return string|null
563 */
564 public function getProjectId(?callable $httpHandler = null)
565 {
566 if ($this->projectId) {
567 return $this->projectId;
568 }
569
570 $httpHandler = $httpHandler
571 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
572
573 if (!$this->hasCheckedOnGce) {
574 $this->isOnGce = self::onGce($httpHandler);
575 $this->hasCheckedOnGce = true;
576 }
577
578 if (!$this->isOnGce) {
579 return null;
580 }
581
582 $this->projectId = $this->getFromMetadata($httpHandler, self::getProjectIdUri());
583 return $this->projectId;
584 }
585
586 /**
587 * Fetch the default universe domain from the metadata server.
588 *
589 * @param callable|null $httpHandler Callback which delivers psr7 request
590 * @return string
591 */
592 public function getUniverseDomain(?callable $httpHandler = null): string
593 {
594 if (null !== $this->universeDomain) {
595 return $this->universeDomain;
596 }
597
598 $httpHandler = $httpHandler
599 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
600
601 if (!$this->hasCheckedOnGce) {
602 $this->isOnGce = self::onGce($httpHandler);
603 $this->hasCheckedOnGce = true;
604 }
605
606 try {
607 $this->universeDomain = $this->getFromMetadata(
608 $httpHandler,
609 self::getUniverseDomainUri()
610 );
611 } catch (ClientException $e) {
612 // If the metadata server exists, but returns a 404 for the universe domain, the auth
613 // libraries should safely assume this is an older metadata server running in GCU, and
614 // should return the default universe domain.
615 if (!$e->hasResponse() || 404 != $e->getResponse()->getStatusCode()) {
616 throw $e;
617 }
618 $this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
619 }
620
621 // We expect in some cases the metadata server will return an empty string for the universe
622 // domain. In this case, the auth library MUST return the default universe domain.
623 if ('' === $this->universeDomain) {
624 $this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
625 }
626
627 return $this->universeDomain;
628 }
629
630 /**
631 * Fetch the value of a GCE metadata server URI.
632 *
633 * @param callable $httpHandler An HTTP Handler to deliver PSR7 requests.
634 * @param string $uri The metadata URI.
635 * @param array<mixed> $headers [optional] If present, add these headers to the token
636 * endpoint request.
637 *
638 * @return string
639 */
640 private function getFromMetadata(callable $httpHandler, $uri, array $headers = [])
641 {
642 $resp = $httpHandler(
643 new Request(
644 'GET',
645 $uri,
646 [self::FLAVOR_HEADER => 'Google'] + $headers
647 )
648 );
649
650 return (string) $resp->getBody();
651 }
652
653 /**
654 * Get the quota project used for this API request
655 *
656 * @return string|null
657 */
658 public function getQuotaProject()
659 {
660 return $this->quotaProject;
661 }
662
663 /**
664 * Set whether or not we've already checked the GCE environment.
665 *
666 * @param bool $isOnGce
667 *
668 * @return void
669 */
670 public function setIsOnGce($isOnGce)
671 {
672 // Implicitly set hasCheckedGce to true
673 $this->hasCheckedOnGce = true;
674
675 // Set isOnGce
676 $this->isOnGce = $isOnGce;
677 }
678
679 protected function getCredType(): string
680 {
681 return self::CRED_TYPE;
682 }
683}
Note: See TracBrowser for help on using the repository browser.