← All changes
|
includes/sdk/s3/Aws/Credentials/CredentialProvider.php
+128
-42
1.2.3
→
1.4.2
View file →
| @@ -48,11 +48,15 @@ | ||
| 48 | 48 | const ENV_KEY = 'AWS_ACCESS_KEY_ID'; |
| 49 | 49 | const ENV_PROFILE = 'AWS_PROFILE'; |
| 50 | 50 | const ENV_ROLE_SESSION_NAME = 'AWS_ROLE_SESSION_NAME'; |
| 51 | 51 | const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY'; |
| 52 | + const ENV_ACCOUNT_ID = 'AWS_ACCOUNT_ID'; | |
| 52 | 53 | const ENV_SESSION = 'AWS_SESSION_TOKEN'; |
| 53 | 54 | const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE'; |
| 54 | 55 | const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE'; |
| 56 | + public const ENV_REGION = 'AWS_REGION'; | |
| 57 | + public const FALLBACK_REGION = 'us-east-1'; | |
| 58 | + public const REFRESH_WINDOW = 60; | |
| 55 | 59 | /** |
| 56 | 60 | * Create a default credential provider that |
| 57 | 61 | * first checks for environment variables, |
| 58 | 62 | * then checks for assumed role via web identity, |
| @@ -78,11 +82,11 @@ | ||
| 78 | 82 | $cacheable = ['web_identity', 'sso', 'process_credentials', 'process_config', 'ecs', 'instance']; |
| 79 | 83 | $profileName = \getenv(self::ENV_PROFILE) ?: 'default'; |
| 80 | 84 | $defaultChain = ['env' => self::env(), 'web_identity' => self::assumeRoleWithWebIdentityCredentialProvider($config)]; |
| 81 | 85 | if (!isset($config['use_aws_shared_config_files']) || $config['use_aws_shared_config_files'] !== \false) { |
| 82 | - $defaultChain['sso'] = self::sso('profile ' . $profileName, self::getHomeDir() . '/.aws/config', $config); | |
| 86 | + $defaultChain['sso'] = self::sso($profileName, self::getHomeDir() . '/.aws/config', $config); | |
| 83 | 87 | $defaultChain['process_credentials'] = self::process(); |
| 84 | - $defaultChain['ini'] = self::ini(); | |
| 88 | + $defaultChain['ini'] = self::ini(null, null, $config); | |
| 85 | 89 | $defaultChain['process_config'] = self::process('profile ' . $profileName, self::getHomeDir() . '/.aws/config'); |
| 86 | 90 | $defaultChain['ini_config'] = self::ini('profile ' . $profileName, self::getHomeDir() . '/.aws/config'); |
| 87 | 91 | } |
| 88 | 92 | if (self::shouldUseEcs()) { |
| @@ -171,10 +175,12 @@ | ||
| 171 | 175 | if (!$creds->getExpiration()) { |
| 172 | 176 | $isConstant = \true; |
| 173 | 177 | return $creds; |
| 174 | 178 | } |
| 175 | - // Refresh expired credentials. | |
| 176 | - if (!$creds->isExpired()) { | |
| 179 | + // Check if credentials are expired or will expire in 1 minute | |
| 180 | + $needsRefresh = $creds->getExpiration() - \time() <= self::REFRESH_WINDOW; | |
| 181 | + // Refresh if expired or expiring soon | |
| 182 | + if (!$needsRefresh && !$creds->isExpired()) { | |
| 177 | 183 | return $creds; |
| 178 | 184 | } |
| 179 | 185 | // Refresh the result and forward the promise. |
| 180 | 186 | return $result = $provider($creds); |
| @@ -221,10 +227,12 @@ | ||
| 221 | 227 | return function () { |
| 222 | 228 | // Use credentials from environment variables, if available |
| 223 | 229 | $key = \getenv(self::ENV_KEY); |
| 224 | 230 | $secret = \getenv(self::ENV_SECRET); |
| 231 | + $accountId = \getenv(self::ENV_ACCOUNT_ID) ?: null; | |
| 232 | + $token = \getenv(self::ENV_SESSION) ?: null; | |
| 225 | 233 | if ($key && $secret) { |
| 226 | - return Promise\Create::promiseFor(new Credentials($key, $secret, \getenv(self::ENV_SESSION) ?: NULL)); | |
| 234 | + return Promise\Create::promiseFor(new Credentials($key, $secret, $token, null, $accountId, CredentialSources::ENVIRONMENT)); | |
| 227 | 235 | } |
| 228 | 236 | return self::reject('Could not find environment variable ' . 'credentials in ' . self::ENV_KEY . '/' . self::ENV_SECRET); |
| 229 | 237 | }; |
| 230 | 238 | } |
| @@ -245,9 +253,9 @@ | ||
| 245 | 253 | * Credential provider that retrieves cached SSO credentials from the CLI |
| 246 | 254 | * |
| 247 | 255 | * @return callable |
| 248 | 256 | */ |
| 249 | - public static function sso($ssoProfileName, $filename = null, $config = []) | |
| 257 | + public static function sso($ssoProfileName = 'default', $filename = null, $config = []) | |
| 250 | 258 | { |
| 251 | 259 | $filename = $filename ?: self::getHomeDir() . '/.aws/config'; |
| 252 | 260 | return function () use($ssoProfileName, $filename, $config) { |
| 253 | 261 | if (!@\is_readable($filename)) { |
| @@ -253,41 +261,21 @@ | ||
| 253 | 261 | if (!@\is_readable($filename)) { |
| 254 | 262 | return self::reject("Cannot read credentials from {$filename}"); |
| 255 | 263 | } |
| 256 | 264 | $profiles = self::loadProfiles($filename); |
| 257 | - if (!isset($profiles[$ssoProfileName])) { | |
| 265 | + if (isset($profiles[$ssoProfileName])) { | |
| 266 | + $ssoProfile = $profiles[$ssoProfileName]; | |
| 267 | + } elseif (isset($profiles['profile ' . $ssoProfileName])) { | |
| 268 | + $ssoProfileName = 'profile ' . $ssoProfileName; | |
| 269 | + $ssoProfile = $profiles[$ssoProfileName]; | |
| 270 | + } else { | |
| 258 | 271 | return self::reject("Profile {$ssoProfileName} does not exist in {$filename}."); |
| 259 | 272 | } |
| 260 | - $ssoProfile = $profiles[$ssoProfileName]; | |
| 261 | - if (empty($ssoProfile['sso_start_url']) || empty($ssoProfile['sso_region']) || empty($ssoProfile['sso_account_id']) || empty($ssoProfile['sso_role_name'])) { | |
| 262 | - return self::reject("Profile {$ssoProfileName} in {$filename} must contain the following keys: " . "sso_start_url, sso_region, sso_account_id, and sso_role_name."); | |
| 263 | - } | |
| 264 | - $tokenLocation = self::getHomeDir() . '/.aws/sso/cache/' . \utf8_encode(\sha1($ssoProfile['sso_start_url'])) . ".json"; | |
| 265 | - if (!@\is_readable($tokenLocation)) { | |
| 266 | - return self::reject("Unable to read token file at {$tokenLocation}"); | |
| 267 | - } | |
| 268 | - $tokenData = \json_decode(\file_get_contents($tokenLocation), \true); | |
| 269 | - if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) { | |
| 270 | - return self::reject("Token file at {$tokenLocation} must contain an access token and an expiration"); | |
| 271 | - } | |
| 272 | - try { | |
| 273 | - $expiration = (new DateTimeResult($tokenData['expiresAt']))->getTimestamp(); | |
| 274 | - } catch (\Exception $e) { | |
| 275 | - return self::reject("Cached SSO credentials returned an invalid expiration"); | |
| 276 | - } | |
| 277 | - $now = \time(); | |
| 278 | - if ($expiration < $now) { | |
| 279 | - return self::reject("Cached SSO credentials returned expired credentials"); | |
| 280 | - } | |
| 281 | - $ssoClient = null; | |
| 282 | - if (empty($config['ssoClient'])) { | |
| 283 | - $ssoClient = new Aws\SSO\SSOClient(['region' => $ssoProfile['sso_region'], 'version' => '2019-06-10', 'credentials' => \false]); | |
| 273 | + if (!empty($ssoProfile['sso_session'])) { | |
| 274 | + return CredentialProvider::getSsoCredentials($profiles, $ssoProfileName, $filename, $config); | |
| 284 | 275 | } else { |
| 285 | - $ssoClient = $config['ssoClient']; | |
| 276 | + return CredentialProvider::getSsoCredentialsLegacy($profiles, $ssoProfileName, $filename, $config); | |
| 286 | 277 | } |
| 287 | - $ssoResponse = $ssoClient->getRoleCredentials(['accessToken' => $tokenData['accessToken'], 'accountId' => $ssoProfile['sso_account_id'], 'roleName' => $ssoProfile['sso_role_name']]); | |
| 288 | - $ssoCredentials = $ssoResponse['roleCredentials']; | |
| 289 | - return Promise\Create::promiseFor(new Credentials($ssoCredentials['accessKeyId'], $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], $expiration)); | |
| 290 | 278 | }; |
| 291 | 279 | } |
| 292 | 280 | /** |
| 293 | 281 | * Credential provider that creates credentials using |
| @@ -331,9 +319,9 @@ | ||
| 331 | 319 | $stsClient = isset($config['stsClient']) ? $config['stsClient'] : null; |
| 332 | 320 | $region = isset($config['region']) ? $config['region'] : null; |
| 333 | 321 | if ($tokenFromEnv && $arnFromEnv) { |
| 334 | 322 | $sessionName = \getenv(self::ENV_ROLE_SESSION_NAME) ? \getenv(self::ENV_ROLE_SESSION_NAME) : null; |
| 335 | - $provider = new AssumeRoleWithWebIdentityCredentialProvider(['RoleArn' => $arnFromEnv, 'WebIdentityTokenFile' => $tokenFromEnv, 'SessionName' => $sessionName, 'client' => $stsClient, 'region' => $region]); | |
| 323 | + $provider = new AssumeRoleWithWebIdentityCredentialProvider(['RoleArn' => $arnFromEnv, 'WebIdentityTokenFile' => $tokenFromEnv, 'SessionName' => $sessionName, 'client' => $stsClient, 'region' => $region, 'source' => CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN]); | |
| 336 | 324 | return $provider(); |
| 337 | 325 | } |
| 338 | 326 | $profileName = \getenv(self::ENV_PROFILE) ?: 'default'; |
| 339 | 327 | if (isset($config['filename'])) { |
| @@ -347,9 +335,9 @@ | ||
| 347 | 335 | $region = $profile['region']; |
| 348 | 336 | } |
| 349 | 337 | if (isset($profile['web_identity_token_file']) && isset($profile['role_arn'])) { |
| 350 | 338 | $sessionName = isset($profile['role_session_name']) ? $profile['role_session_name'] : null; |
| 351 | - $provider = new AssumeRoleWithWebIdentityCredentialProvider(['RoleArn' => $profile['role_arn'], 'WebIdentityTokenFile' => $profile['web_identity_token_file'], 'SessionName' => $sessionName, 'client' => $stsClient, 'region' => $region]); | |
| 339 | + $provider = new AssumeRoleWithWebIdentityCredentialProvider(['RoleArn' => $profile['role_arn'], 'WebIdentityTokenFile' => $profile['web_identity_token_file'], 'SessionName' => $sessionName, 'client' => $stsClient, 'region' => $region, 'source' => CredentialSources::PROFILE_STS_WEB_ID_TOKEN]); | |
| 352 | 340 | return $provider(); |
| 353 | 341 | } |
| 354 | 342 | } else { |
| 355 | 343 | return self::reject("Unknown profile: {$profileName}"); |
| @@ -416,9 +404,9 @@ | ||
| 416 | 404 | } |
| 417 | 405 | if (empty($data[$profile]['aws_session_token'])) { |
| 418 | 406 | $data[$profile]['aws_session_token'] = isset($data[$profile]['aws_security_token']) ? $data[$profile]['aws_security_token'] : null; |
| 419 | 407 | } |
| 420 | - return Promise\Create::promiseFor(new Credentials($data[$profile]['aws_access_key_id'], $data[$profile]['aws_secret_access_key'], $data[$profile]['aws_session_token'])); | |
| 408 | + return Promise\Create::promiseFor(new Credentials($data[$profile]['aws_access_key_id'], $data[$profile]['aws_secret_access_key'], $data[$profile]['aws_session_token'], null, $data[$profile]['aws_account_id'] ?? null, CredentialSources::PROFILE)); | |
| 421 | 409 | }; |
| 422 | 410 | } |
| 423 | 411 | /** |
| 424 | 412 | * Credentials provider that creates credentials using a process configured in |
| @@ -477,9 +465,15 @@ | ||
| 477 | 465 | } |
| 478 | 466 | if (empty($processData['SessionToken'])) { |
| 479 | 467 | $processData['SessionToken'] = null; |
| 480 | 468 | } |
| 481 | - return Promise\Create::promiseFor(new Credentials($processData['AccessKeyId'], $processData['SecretAccessKey'], $processData['SessionToken'], $expires)); | |
| 469 | + $accountId = null; | |
| 470 | + if (!empty($processData['AccountId'])) { | |
| 471 | + $accountId = $processData['AccountId']; | |
| 472 | + } elseif (!empty($data[$profile]['aws_account_id'])) { | |
| 473 | + $accountId = $data[$profile]['aws_account_id']; | |
| 474 | + } | |
| 475 | + return Promise\Create::promiseFor(new Credentials($processData['AccessKeyId'], $processData['SecretAccessKey'], $processData['SessionToken'], $expires, $accountId, CredentialSources::PROFILE_PROCESS)); | |
| 482 | 476 | }; |
| 483 | 477 | } |
| 484 | 478 | /** |
| 485 | 479 | * Assumes role for profile that includes role_arn |
| @@ -509,9 +503,8 @@ | ||
| 509 | 503 | return self::reject("A role_arn must be provided with credential_source in " . "file {$filename} under profile {$profileName} "); |
| 510 | 504 | } |
| 511 | 505 | } |
| 512 | 506 | if (empty($stsClient)) { |
| 513 | - $sourceRegion = isset($profiles[$sourceProfileName]['region']) ? $profiles[$sourceProfileName]['region'] : 'us-east-1'; | |
| 514 | 507 | $config['preferStaticCredentials'] = \true; |
| 515 | 508 | $sourceCredentials = null; |
| 516 | 509 | if (!empty($roleProfile['source_profile'])) { |
| 517 | 510 | $sourceCredentials = \call_user_func(CredentialProvider::ini($sourceProfileName, $filename, $config))->wait(); |
| @@ -517,12 +510,13 @@ | ||
| 517 | 510 | $sourceCredentials = \call_user_func(CredentialProvider::ini($sourceProfileName, $filename, $config))->wait(); |
| 518 | 511 | } else { |
| 519 | 512 | $sourceCredentials = self::getCredentialsFromSource($profileName, $filename); |
| 520 | 513 | } |
| 521 | - $stsClient = new StsClient(['credentials' => $sourceCredentials, 'region' => $sourceRegion, 'version' => '2011-06-15']); | |
| 514 | + $region = $profiles[$sourceProfileName]['region'] ?? $config['region'] ?? \getEnv(self::ENV_REGION) ?: null; | |
| 515 | + $stsClient = self::createDefaultStsClient($sourceCredentials, $region); | |
| 522 | 516 | } |
| 523 | 517 | $result = $stsClient->assumeRole(['RoleArn' => $roleArn, 'RoleSessionName' => $roleSessionName]); |
| 524 | - $credentials = $stsClient->createCredentials($result); | |
| 518 | + $credentials = $stsClient->createCredentials($result, CredentialSources::STS_ASSUME_ROLE); | |
| 525 | 519 | return Promise\Create::promiseFor($credentials); |
| 526 | 520 | } |
| 527 | 521 | /** |
| 528 | 522 | * Gets the environment's HOME directory if available. |
| @@ -633,6 +627,98 @@ | ||
| 633 | 627 | { |
| 634 | 628 | //Check for relative uri. if not, then full uri. |
| 635 | 629 | //fall back to server for each as getenv is not thread-safe. |
| 636 | 630 | return !empty(\getenv(EcsCredentialProvider::ENV_URI)) || !empty($_SERVER[EcsCredentialProvider::ENV_URI]) || !empty(\getenv(EcsCredentialProvider::ENV_FULL_URI)) || !empty($_SERVER[EcsCredentialProvider::ENV_FULL_URI]); |
| 631 | + } | |
| 632 | + /** | |
| 633 | + * @param $profiles | |
| 634 | + * @param $ssoProfileName | |
| 635 | + * @param $filename | |
| 636 | + * @param $config | |
| 637 | + * @return Promise\PromiseInterface | |
| 638 | + */ | |
| 639 | + private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $config) | |
| 640 | + { | |
| 641 | + if (empty($config['ssoOidcClient'])) { | |
| 642 | + $ssoProfile = $profiles[$ssoProfileName]; | |
| 643 | + $sessionName = $ssoProfile['sso_session']; | |
| 644 | + if (empty($profiles['sso-session ' . $sessionName])) { | |
| 645 | + return self::reject("Could not find sso-session {$sessionName} in {$filename}"); | |
| 646 | + } | |
| 647 | + $ssoSession = $profiles['sso-session ' . $ssoProfile['sso_session']]; | |
| 648 | + $ssoOidcClient = new Aws\SSOOIDC\SSOOIDCClient(['region' => $ssoSession['sso_region'], 'version' => '2019-06-10', 'credentials' => \false]); | |
| 649 | + } else { | |
| 650 | + $ssoOidcClient = $config['ssoClient']; | |
| 651 | + } | |
| 652 | + $tokenPromise = new Aws\Token\SsoTokenProvider($ssoProfileName, $filename, $ssoOidcClient); | |
| 653 | + $token = $tokenPromise()->wait(); | |
| 654 | + $ssoCredentials = CredentialProvider::getCredentialsFromSsoService($ssoProfile, $ssoSession['sso_region'], $token->getToken(), $config); | |
| 655 | + //Expiration value is returned in epoch milliseconds. Conversion to seconds | |
| 656 | + $expiration = \intdiv($ssoCredentials['expiration'], 1000); | |
| 657 | + return Promise\Create::promiseFor(new Credentials($ssoCredentials['accessKeyId'], $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], $expiration, $ssoProfile['sso_account_id'], CredentialSources::PROFILE_SSO)); | |
| 658 | + } | |
| 659 | + /** | |
| 660 | + * @param $profiles | |
| 661 | + * @param $ssoProfileName | |
| 662 | + * @param $filename | |
| 663 | + * @param $config | |
| 664 | + * @return Promise\PromiseInterface | |
| 665 | + */ | |
| 666 | + private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $filename, $config) | |
| 667 | + { | |
| 668 | + $ssoProfile = $profiles[$ssoProfileName]; | |
| 669 | + if (empty($ssoProfile['sso_start_url']) || empty($ssoProfile['sso_region']) || empty($ssoProfile['sso_account_id']) || empty($ssoProfile['sso_role_name'])) { | |
| 670 | + return self::reject("Profile {$ssoProfileName} in {$filename} must contain the following keys: " . "sso_start_url, sso_region, sso_account_id, and sso_role_name."); | |
| 671 | + } | |
| 672 | + $tokenLocation = self::getHomeDir() . '/.aws/sso/cache/' . \sha1($ssoProfile['sso_start_url']) . ".json"; | |
| 673 | + if (!@\is_readable($tokenLocation)) { | |
| 674 | + return self::reject("Unable to read token file at {$tokenLocation}"); | |
| 675 | + } | |
| 676 | + $tokenData = \json_decode(\file_get_contents($tokenLocation), \true); | |
| 677 | + if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) { | |
| 678 | + return self::reject("Token file at {$tokenLocation} must contain an access token and an expiration"); | |
| 679 | + } | |
| 680 | + try { | |
| 681 | + $expiration = (new DateTimeResult($tokenData['expiresAt']))->getTimestamp(); | |
| 682 | + } catch (\Exception $e) { | |
| 683 | + return self::reject("Cached SSO credentials returned an invalid expiration"); | |
| 684 | + } | |
| 685 | + $now = \time(); | |
| 686 | + if ($expiration < $now) { | |
| 687 | + return self::reject("Cached SSO credentials returned expired credentials"); | |
| 688 | + } | |
| 689 | + $ssoCredentials = CredentialProvider::getCredentialsFromSsoService($ssoProfile, $ssoProfile['sso_region'], $tokenData['accessToken'], $config); | |
| 690 | + return Promise\Create::promiseFor(new Credentials($ssoCredentials['accessKeyId'], $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], $expiration, $ssoProfile['sso_account_id'], CredentialSources::PROFILE_SSO_LEGACY)); | |
| 691 | + } | |
| 692 | + /** | |
| 693 | + * @param array $ssoProfile | |
| 694 | + * @param string $clientRegion | |
| 695 | + * @param string $accessToken | |
| 696 | + * @param array $config | |
| 697 | + * @return array|null | |
| 698 | + */ | |
| 699 | + private static function getCredentialsFromSsoService($ssoProfile, $clientRegion, $accessToken, $config) | |
| 700 | + { | |
| 701 | + if (empty($config['ssoClient'])) { | |
| 702 | + $ssoClient = new Aws\SSO\SSOClient(['region' => $clientRegion, 'version' => '2019-06-10', 'credentials' => \false]); | |
| 703 | + } else { | |
| 704 | + $ssoClient = $config['ssoClient']; | |
| 705 | + } | |
| 706 | + $ssoResponse = $ssoClient->getRoleCredentials(['accessToken' => $accessToken, 'accountId' => $ssoProfile['sso_account_id'], 'roleName' => $ssoProfile['sso_role_name']]); | |
| 707 | + $ssoCredentials = $ssoResponse['roleCredentials']; | |
| 708 | + return $ssoCredentials; | |
| 709 | + } | |
| 710 | + /** | |
| 711 | + * @param CredentialsInterface $credentials | |
| 712 | + * @param string|null $region | |
| 713 | + * | |
| 714 | + * @return StsClient | |
| 715 | + */ | |
| 716 | + private static function createDefaultStsClient(CredentialsInterface|callable $credentials, ?string $region) : StsClient | |
| 717 | + { | |
| 718 | + if (empty($region)) { | |
| 719 | + $region = self::FALLBACK_REGION; | |
| 720 | + \trigger_error('NOTICE: STS client created without explicit `region` configuration.' . \PHP_EOL . "Defaulting to `{$region}`. This fallback behavior may be removed." . \PHP_EOL . 'To avoid potential disruptions, configure a `region` using one of the following methods:' . \PHP_EOL . '(1) Add `region` to your source profile in ~/.aws/credentials,' . \PHP_EOL . '(2) Pass `region` in the `$config` array when calling the provider,' . \PHP_EOL . '(3) Set the `AWS_REGION` environment variable.' . \PHP_EOL . 'See: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_assume_role.html#assume-role-with-profile' . \PHP_EOL, \E_USER_NOTICE); | |
| 721 | + } | |
| 722 | + return new StsClient(['credentials' => $credentials, 'region' => $region]); | |
| 637 | 723 | } |
| 638 | 724 | } |