diff --git a/.changes/nextrelease/feat_account_id_endpoint_support.json b/.changes/nextrelease/feat_account_id_endpoint_support.json new file mode 100644 index 0000000000..cc84b6f747 --- /dev/null +++ b/.changes/nextrelease/feat_account_id_endpoint_support.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "Endpoint", + "description": "Endpoint resolution based on a account id." + } +] diff --git a/src/AwsClient.php b/src/AwsClient.php index 49a6f1f204..56eeb912cd 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -211,6 +211,16 @@ public static function getArguments() * client-side parameter validation. * - version: (string, required) The version of the webservice to * utilize (e.g., 2006-03-01). + * - account_id_endpoint_mode: (string, default(preferred)) this option + * decides whether credentials should resolve an accountId value, + * which is going to be used as part of the endpoint resolution. + * The valid values for this option are: + * - preferred: when this value is set then, a warning is logged when + * accountId is empty in the resolved identity. + * - required: when this value is set then, an exception is thrown when + * accountId is empty in the resolved identity. + * - disabled: when this value is set then, the validation for if accountId + * was resolved or not, is ignored. * - ua_append: (string, array) To pass custom user agent parameters. * - app_id: (string) an optional application specific identifier that can be set. * When set it will be appended to the User-Agent header of every request @@ -245,7 +255,7 @@ public function __construct(array $args) $this->region = $config['region'] ?? null; $this->signingRegionSet = $config['sigv4a_signing_region_set'] ?? null; $this->config = $config['config']; - $this->setClientBuiltIns($args); + $this->setClientBuiltIns($args, $config); $this->clientContextParams = $this->setClientContextParams($args); $this->defaultRequestOptions = $config['http']; $this->endpointProvider = $config['endpoint_provider']; @@ -578,7 +588,8 @@ private function addEndpointV2Middleware() EndpointV2Middleware::wrap( $this->endpointProvider, $this->getApi(), - $endpointArgs + $endpointArgs, + $this->credentialProvider ), 'endpoint-resolution' ); @@ -608,10 +619,10 @@ private function setClientContextParams($args) /** * Retrieves and sets default values used for endpoint resolution. */ - private function setClientBuiltIns($args) + private function setClientBuiltIns($args, $resolvedConfig) { $builtIns = []; - $config = $this->getConfig(); + $config = $resolvedConfig['config']; $service = $args['service']; $builtIns['SDK::Endpoint'] = null; @@ -632,6 +643,8 @@ private function setClientBuiltIns($args) $builtIns['AWS::S3::ForcePathStyle'] = $config['use_path_style_endpoint']; $builtIns['AWS::S3::DisableMultiRegionAccessPoints'] = $config['disable_multiregion_access_points']; } + $builtIns['AWS::Auth::AccountIdEndpointMode'] = $resolvedConfig['account_id_endpoint_mode']; + $this->clientBuiltIns += $builtIns; } diff --git a/src/ClientResolver.php b/src/ClientResolver.php index fce19df7e6..29db62c70b 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -308,6 +308,13 @@ class ClientResolver 'doc' => 'Set to false to disable checking for shared aws config files usually located in \'~/.aws/config\' and \'~/.aws/credentials\'. This will be ignored if you set the \'profile\' setting.', 'default' => true, ], + 'account_id_endpoint_mode' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'Decides whether account_id must a be a required resolved credentials property. If this configuration is set to disabled, then account_id is not required. If set to preferred a warning will be logged when account_id is not resolved, and when set to required an exception will be thrown if account_id is not resolved.', + 'default' => [__CLASS__, '_default_account_id_endpoint_mode'], + 'fn' => [__CLASS__, '_apply_account_id_endpoint_mode'] + ], 'sigv4a_signing_region_set' => [ 'type' => 'value', 'valid' => ['array', 'string'], @@ -625,7 +632,8 @@ public static function _apply_credentials($value, array &$args) $value['key'], $value['secret'], $value['token'] ?? null, - $value['expires'] ?? null + $value['expires'] ?? null, + $value['accountId'] ?? null ) ); } elseif ($value === false) { @@ -1095,6 +1103,29 @@ public static function _apply_idempotency_auto_fill( } } + public static function _default_account_id_endpoint_mode($args) + { + return ConfigurationResolver::resolve( + 'account_id_endpoint_mode', + 'preferred', + 'string', + $args + ); + } + + public static function _apply_account_id_endpoint_mode($value, array &$args) + { + static $accountIdEndpointModes = ['disabled', 'required', 'preferred']; + if (!in_array($value, $accountIdEndpointModes)) { + throw new IAE( + "The value provided for the config account_id_endpoint_mode is invalid." + ."Valid values are: " . implode(", ", $accountIdEndpointModes) + ); + } + + $args['account_id_endpoint_mode'] = $value; + } + public static function _default_endpoint_provider(array $args) { $service = $args['api'] ?? null; diff --git a/src/Credentials/CredentialProvider.php b/src/Credentials/CredentialProvider.php index 1647fe0da9..57238f0562 100644 --- a/src/Credentials/CredentialProvider.php +++ b/src/Credentials/CredentialProvider.php @@ -48,6 +48,7 @@ class CredentialProvider const ENV_PROFILE = 'AWS_PROFILE'; const ENV_ROLE_SESSION_NAME = 'AWS_ROLE_SESSION_NAME'; const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY'; + const ENV_ACCOUNT_ID = 'AWS_ACCOUNT_ID'; const ENV_SESSION = 'AWS_SESSION_TOKEN'; const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE'; const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE'; @@ -291,9 +292,18 @@ public static function env() // Use credentials from environment variables, if available $key = getenv(self::ENV_KEY); $secret = getenv(self::ENV_SECRET); + $accountId = getenv(self::ENV_ACCOUNT_ID) ?: null; + $token = getenv(self::ENV_SESSION) ?: null; + if ($key && $secret) { return Promise\Create::promiseFor( - new Credentials($key, $secret, getenv(self::ENV_SESSION) ?: NULL) + new Credentials( + $key, + $secret, + $token, + null, + $accountId + ) ); } @@ -541,7 +551,9 @@ public static function ini($profile = null, $filename = null, array $config = [] new Credentials( $data[$profile]['aws_access_key_id'], $data[$profile]['aws_secret_access_key'], - $data[$profile]['aws_session_token'] + $data[$profile]['aws_session_token'], + null, + !empty($data[$profile]['aws_account_id']) ? $data[$profile]['aws_account_id'] : null ) ); }; @@ -616,12 +628,20 @@ public static function process($profile = null, $filename = null) $processData['SessionToken'] = null; } + $accountId = null; + if (!empty($processData['AccountId'])) { + $accountId = $processData['AccountId']; + } elseif (!empty($data[$profile]['aws_account_id'])) { + $accountId = $data[$profile]['aws_account_id']; + } + return Promise\Create::promiseFor( new Credentials( $processData['AccessKeyId'], $processData['SecretAccessKey'], $processData['SessionToken'], - $expires + $expires, + $accountId ) ); }; @@ -704,8 +724,8 @@ private static function loadRoleProfile( 'RoleArn' => $roleArn, 'RoleSessionName' => $roleSessionName ]); - $credentials = $stsClient->createCredentials($result); + return Promise\Create::promiseFor($credentials); } @@ -897,7 +917,8 @@ private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $ssoCredentials['accessKeyId'], $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], - $expiration + $expiration, + $ssoProfile['sso_account_id'] ) ); } @@ -956,7 +977,8 @@ private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $fil $ssoCredentials['accessKeyId'], $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], - $expiration + $expiration, + $ssoProfile['sso_account_id'] ) ); } diff --git a/src/Credentials/Credentials.php b/src/Credentials/Credentials.php index fc11c3cd66..059d307121 100644 --- a/src/Credentials/Credentials.php +++ b/src/Credentials/Credentials.php @@ -15,6 +15,7 @@ class Credentials extends AwsCredentialIdentity implements private $secret; private $token; private $expires; + private $accountId; /** * Constructs a new BasicAWSCredentials object, with the specified AWS @@ -25,12 +26,13 @@ class Credentials extends AwsCredentialIdentity implements * @param string $token Security token to use * @param int $expires UNIX timestamp for when credentials expire */ - public function __construct($key, $secret, $token = null, $expires = null) + public function __construct($key, $secret, $token = null, $expires = null, $accountId = null) { $this->key = trim((string) $key); $this->secret = trim((string) $secret); $this->token = $token; $this->expires = $expires; + $this->accountId = $accountId; } public static function __set_state(array $state) @@ -39,7 +41,8 @@ public static function __set_state(array $state) $state['key'], $state['secret'], $state['token'], - $state['expires'] + $state['expires'], + $state['accountId'] ); } @@ -68,13 +71,19 @@ public function isExpired() return $this->expires !== null && time() >= $this->expires; } + public function getAccountId() + { + return $this->accountId; + } + public function toArray() { return [ 'key' => $this->key, 'secret' => $this->secret, 'token' => $this->token, - 'expires' => $this->expires + 'expires' => $this->expires, + 'accountId' => $this->accountId ]; } @@ -101,6 +110,7 @@ public function __unserialize($data) $this->secret = $data['secret']; $this->token = $data['token']; $this->expires = $data['expires']; + $this->accountId = $data['accountId']; } /** diff --git a/src/Credentials/EcsCredentialProvider.php b/src/Credentials/EcsCredentialProvider.php index 33ebe08cd3..893ee09b25 100644 --- a/src/Credentials/EcsCredentialProvider.php +++ b/src/Credentials/EcsCredentialProvider.php @@ -69,18 +69,13 @@ public function __construct(array $config = []) public function __invoke() { $this->attempts = 0; - $uri = $this->getEcsUri(); - if ($this->isCompatibleUri($uri)) { return Promise\Coroutine::of(function () { $client = $this->client; $request = new Request('GET', $this->getEcsUri()); - $headers = $this->getHeadersForAuthToken(); - $credentials = null; - while ($credentials === null) { $credentials = (yield $client( $request, @@ -95,7 +90,8 @@ public function __invoke() $result['AccessKeyId'], $result['SecretAccessKey'], $result['Token'], - strtotime($result['Expiration']) + strtotime($result['Expiration']), + $result['AccountId'] ?? null ); })->otherwise(function ($reason) { $reason = is_array($reason) ? $reason['exception'] : $reason; diff --git a/src/Credentials/InstanceProfileProvider.php b/src/Credentials/InstanceProfileProvider.php index 1fb5712b2f..7a7a178b6f 100644 --- a/src/Credentials/InstanceProfileProvider.php +++ b/src/Credentials/InstanceProfileProvider.php @@ -226,7 +226,8 @@ public function __invoke($previousCredentials = null) $result['AccessKeyId'], $result['SecretAccessKey'], $result['Token'], - strtotime($result['Expiration']) + strtotime($result['Expiration']), + $result['AccountId'] ?? null ); } diff --git a/src/EndpointV2/EndpointV2Middleware.php b/src/EndpointV2/EndpointV2Middleware.php index 3e897d0b71..e3070a7d90 100644 --- a/src/EndpointV2/EndpointV2Middleware.php +++ b/src/EndpointV2/EndpointV2Middleware.php @@ -18,6 +18,8 @@ */ class EndpointV2Middleware { + const ACCOUNT_ID_PARAM = 'AccountId'; + const ACCOUNT_ID_ENDPOINT_MODE_PARAM = 'AccountIdEndpointMode'; private static $validAuthSchemes = [ 'sigv4' => 'v4', 'sigv4a' => 'v4a', @@ -38,23 +40,28 @@ class EndpointV2Middleware /** @var array */ private $clientArgs; + /** @var Closure */ + private $credentialProvider; + /** * Create a middleware wrapper function * * @param EndpointProviderV2 $endpointProvider * @param Service $api * @param array $args + * @param callable $credentialProvider * * @return Closure */ public static function wrap( EndpointProviderV2 $endpointProvider, Service $api, - array $args - ): Closure + array $args, + callable $credentialProvider + ) : Closure { - return function (callable $handler) use ($endpointProvider, $api, $args) { - return new self($handler, $endpointProvider, $api, $args); + return function (callable $handler) use ($endpointProvider, $api, $args, $credentialProvider) { + return new self($handler, $endpointProvider, $api, $args, $credentialProvider); }; } @@ -68,13 +75,15 @@ public function __construct( callable $nextHandler, EndpointProviderV2 $endpointProvider, Service $api, - array $args + array $args, + callable $credentialProvider = null ) { $this->nextHandler = $nextHandler; $this->endpointProvider = $endpointProvider; $this->api = $api; $this->clientArgs = $args; + $this->credentialProvider = $credentialProvider; } /** @@ -87,7 +96,6 @@ public function __invoke(CommandInterface $command) $nextHandler = $this->nextHandler; $operation = $this->api->getOperation($command->getName()); $commandArgs = $command->toArray(); - $providerArgs = $this->resolveArgs($commandArgs, $operation); $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs); @@ -113,6 +121,12 @@ public function __invoke(CommandInterface $command) private function resolveArgs(array $commandArgs, Operation $operation): array { $rulesetParams = $this->endpointProvider->getRuleset()->getParameters(); + + if (isset($rulesetParams[self::ACCOUNT_ID_PARAM]) + && isset($rulesetParams[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM])) { + $this->clientArgs[self::ACCOUNT_ID_PARAM] = $this->resolveAccountId(); + } + $endpointCommandArgs = $this->filterEndpointCommandArgs( $rulesetParams, $commandArgs @@ -320,6 +334,31 @@ private function isValidAuthScheme($signatureVersion): bool } return true; } + return false; } + + /** + * This method tries to resolve an `AccountId` parameter from a resolved identity. + * We will just perform this operation if the parameter `AccountId` is part of the ruleset parameters and + * `AccountIdEndpointMode` is not disabled, otherwise, we will ignore it. + * + * @return null|string + */ + private function resolveAccountId(): ?string + { + if (isset($this->clientArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM]) + && $this->clientArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM] === 'disabled') { + return null; + } + + if (is_null($this->credentialProvider)) { + return null; + } + + $identityProviderFn = $this->credentialProvider; + $identity = $identityProviderFn()->wait(); + + return $identity->getAccountId(); + } } diff --git a/src/Sts/StsClient.php b/src/Sts/StsClient.php index 5f22d9452d..568e643edf 100644 --- a/src/Sts/StsClient.php +++ b/src/Sts/StsClient.php @@ -1,6 +1,7 @@ hasKey('AssumedRoleUser')) { + $parsedArn = ArnParser::parse($result->get('AssumedRoleUser')['Arn']); + $accountId = $parsedArn->getAccountId(); + } elseif ($result->hasKey('FederatedUser')) { + $parsedArn = ArnParser::parse($result->get('FederatedUser')['Arn']); + $accountId = $parsedArn->getAccountId(); + } + + $credentials = $result['Credentials']; + $expiration = isset($credentials['Expiration']) && $credentials['Expiration'] instanceof \DateTimeInterface + ? (int) $credentials['Expiration']->format('U') + : null; return new Credentials( - $c['AccessKeyId'], - $c['SecretAccessKey'], - isset($c['SessionToken']) ? $c['SessionToken'] : null, - isset($c['Expiration']) && $c['Expiration'] instanceof \DateTimeInterface - ? (int) $c['Expiration']->format('U') - : null + $credentials['AccessKeyId'], + $credentials['SecretAccessKey'], + isset($credentials['SessionToken']) ? $credentials['SessionToken'] : null, + $expiration, + $accountId ); } diff --git a/tests/AwsClientTest.php b/tests/AwsClientTest.php index 86c34c82c6..2df358aec0 100644 --- a/tests/AwsClientTest.php +++ b/tests/AwsClientTest.php @@ -544,6 +544,7 @@ public function testGetClientBuiltins() 'AWS::UseFIPS' => false, 'AWS::UseDualStack' => false, 'AWS::STS::UseGlobalEndpoint' => true, + 'AWS::Auth::AccountIdEndpointMode' => 'preferred', ]; $builtIns = $client->getClientBuiltIns(); $this->assertEquals( @@ -564,6 +565,7 @@ public function testGetEndpointProviderArgs() 'UseFIPS' => false, 'UseDualStack' => false, 'UseGlobalEndpoint' => true, + 'AccountIdEndpointMode' => 'preferred' ]; $providerArgs = $client->getEndpointProviderArgs(); $this->assertEquals( @@ -948,4 +950,26 @@ private function createClient(array $service = [], array $config = []) 'version' => 'latest' ]); } + + public function testClientDefaultsAccountIdEndpointModeBuiltInsToPreferred() + { + $client = new S3Client([ + 'region' => 'us-east-1' + ]); + $builtIns = $client->getClientBuiltIns(); + + self::assertEquals('preferred', $builtIns['AWS::Auth::AccountIdEndpointMode']); + } + + public function testClientParameterOverridesDefaultAccountIdEndpointModeBuiltIns() + { + $expectedAccountIdEndpointMode = 'required'; + $client = new S3Client([ + 'region' => 'us-east-1', + 'account_id_endpoint_mode' => $expectedAccountIdEndpointMode + ]); + $builtIns = $client->getClientBuiltIns(); + + self::assertEquals($expectedAccountIdEndpointMode, $builtIns['AWS::Auth::AccountIdEndpointMode']); + } } diff --git a/tests/Credentials/AssumeRoleCredentialProviderTest.php b/tests/Credentials/AssumeRoleCredentialProviderTest.php index 0cca58c4b2..3e663d70f8 100644 --- a/tests/Credentials/AssumeRoleCredentialProviderTest.php +++ b/tests/Credentials/AssumeRoleCredentialProviderTest.php @@ -1,6 +1,7 @@ assertNull($creds->getSecurityToken()); $this->assertIsInt($creds->getExpiration()); $this->assertFalse($creds->isExpired()); + $expectedAccountId = ArnParser::parse(self::SAMPLE_ROLE_ARN)->getAccountId(); + $this->assertSame($expectedAccountId, $creds->getAccountId()); } public function testThrowsExceptionWhenRetrievingAssumeRoleCredentialFails() diff --git a/tests/Credentials/AssumeRoleWithWebIdentityCredentialProviderTest.php b/tests/Credentials/AssumeRoleWithWebIdentityCredentialProviderTest.php index d44a3b0bfe..75b1bec837 100644 --- a/tests/Credentials/AssumeRoleWithWebIdentityCredentialProviderTest.php +++ b/tests/Credentials/AssumeRoleWithWebIdentityCredentialProviderTest.php @@ -1,6 +1,7 @@ 'baz', 'Expiration' => DateTimeResult::fromEpoch(time() + 10) ], + 'AssumedRoleUser' => [ + 'AssumedRoleId' => 'test_user_621903f1f21f5.01530789', + 'Arn' => self::SAMPLE_ROLE_ARN + ] ]; $tokenPath = $dir . '/my-token.jwt'; @@ -102,6 +107,8 @@ function ($c, $r) use ($result) { $this->assertSame('baz', $creds->getSecurityToken()); $this->assertIsInt($creds->getExpiration()); $this->assertFalse($creds->isExpired()); + $expectedAccountId = ArnParser::parse(self::SAMPLE_ROLE_ARN)->getAccountId(); + $this->assertSame($expectedAccountId, $creds->getAccountId()); } catch (\Error $e) { throw $e; } finally { diff --git a/tests/Credentials/CredentialProviderTest.php b/tests/Credentials/CredentialProviderTest.php index 1d92e1f7f5..ba8afebda6 100644 --- a/tests/Credentials/CredentialProviderTest.php +++ b/tests/Credentials/CredentialProviderTest.php @@ -20,7 +20,7 @@ */ class CredentialProviderTest extends TestCase { - private $home, $homedrive, $homepath, $key, $secret, $profile; + private $home, $homedrive, $homepath, $key, $secret, $profile, $accountId; private static $standardIni = <<key); putenv(CredentialProvider::ENV_SECRET . '=' . $this->secret); putenv(CredentialProvider::ENV_PROFILE . '=' . $this->profile); + putenv(CredentialProvider::ENV_ACCOUNT_ID . '=' . $this->accountId); } public function testCreatesFromCache() @@ -167,10 +170,13 @@ public function testCreatesFromEnvironmentVariables() putenv(CredentialProvider::ENV_KEY . '=abc'); putenv(CredentialProvider::ENV_SECRET . '=123'); putenv(CredentialProvider::ENV_SESSION . '=456'); + $testAccountId = 'foo'; + putenv(CredentialProvider::ENV_ACCOUNT_ID ."=$testAccountId"); $creds = call_user_func(CredentialProvider::env())->wait(); $this->assertSame('abc', $creds->getAccessKeyId()); $this->assertSame('123', $creds->getSecretKey()); $this->assertSame('456', $creds->getSecurityToken()); + $this->assertSame($testAccountId, $creds->getAccountId()); } public function testCreatesFromEnvironmentVariablesNullToken() @@ -205,6 +211,8 @@ public function testCreatesFromIniFile($iniFile, Credentials $expectedCreds) public function iniFileProvider() { $credentials = new Credentials('foo', 'bar', 'baz'); + $testAccountId = 'foo'; + $credentialsWithAccountId = new Credentials('foo', 'bar', 'baz', null, $testAccountId); $credentialsWithEquals = new Credentials('foo', 'bar', 'baz='); $standardIni = << "foo", "secret" => "bar", "token" => "baz", - "expires" => null + "expires" => null, + "accountId" => null ]; putenv('HOME=' . dirname($dir)); $creds = call_user_func( @@ -359,6 +376,24 @@ public function testCreatesFromProcessCredentialProvider() $this->assertSame('bar', $creds->getSecretKey()); } + public function testCreatesFromProcessCredentialProviderWithAccountId() + { + $testAccountId = 'foo'; + $dir = $this->clearEnv(); + $ini = <<wait(); + unlink($dir . '/credentials'); + $this->assertSame('foo', $creds->getAccessKeyId()); + $this->assertSame('bar', $creds->getSecretKey()); + $this->assertSame($testAccountId, $creds->getAccountId()); + } + public function testCreatesFromProcessCredentialWithFilename() { $dir = $this->clearEnv(); @@ -436,12 +471,14 @@ public function testCreatesFromIniCredentialWithSharedFilename() public function testCreatesFromIniCredentialWithDefaultProvider() { + $testAccountId = 'foo'; $dir = $this->clearEnv(); $ini = <<assertEquals('foo', $creds->getAccessKeyId()); $this->assertEquals('bar', $creds->getSecretKey()); + $this->assertEquals($testAccountId, $creds->getAccountId()); } public function testCreatesTemporaryFromProcessCredential() @@ -1910,7 +1948,7 @@ public function testCachesAsPartOfDefaultChain() 'credentials' => $cache, ])) ->wait(); - + $this->assertSame($ecsCredential->getAccessKeyId(), $credentials->getAccessKeyId()); $this->assertSame($ecsCredential->getSecretKey(), $credentials->getSecretKey()); @@ -1921,7 +1959,7 @@ public function testCachesAsPartOfDefaultChain() 'credentials' => $cache, ])) ->wait(); - + $this->assertSame($ecsCredential->getAccessKeyId(), $credentials->getAccessKeyId()); $this->assertSame($ecsCredential->getSecretKey(), $credentials->getSecretKey()); } diff --git a/tests/Credentials/CredentialsTest.php b/tests/Credentials/CredentialsTest.php index 5b630c2fdc..8e68b4c308 100644 --- a/tests/Credentials/CredentialsTest.php +++ b/tests/Credentials/CredentialsTest.php @@ -15,16 +15,19 @@ class CredentialsTest extends TestCase public function testHasGetters() { $exp = time() + 500; - $creds = new Credentials('foo', 'baz', 'tok', $exp); + $accountId = 'foo'; + $creds = new Credentials('foo', 'baz', 'tok', $exp, $accountId); $this->assertSame('foo', $creds->getAccessKeyId()); $this->assertSame('baz', $creds->getSecretKey()); $this->assertSame('tok', $creds->getSecurityToken()); $this->assertSame($exp, $creds->getExpiration()); + $this->assertSame($accountId, $creds->getAccountId()); $this->assertEquals([ 'key' => 'foo', 'secret' => 'baz', 'token' => 'tok', - 'expires' => $exp + 'expires' => $exp, + 'accountId' => $accountId ], $creds->toArray()); } @@ -48,9 +51,10 @@ public function testSerialization() 'secret' => 'secret-value', 'token' => null, 'expires' => null, + 'accountId' => null ], $actual); - - $credentials = new Credentials('key-value', 'secret-value', 'token-value', 10); + $accountId = 'foo'; + $credentials = new Credentials('key-value', 'secret-value', 'token-value', 10, $accountId); $actual = unserialize(serialize($credentials))->toArray(); $this->assertEquals([ @@ -58,6 +62,7 @@ public function testSerialization() 'secret' => 'secret-value', 'token' => 'token-value', 'expires' => 10, + 'accountId' => $accountId ], $actual); } diff --git a/tests/Credentials/EcsCredentialProviderTest.php b/tests/Credentials/EcsCredentialProviderTest.php index 9c2d2d77e9..78cf54a209 100644 --- a/tests/Credentials/EcsCredentialProviderTest.php +++ b/tests/Credentials/EcsCredentialProviderTest.php @@ -8,6 +8,7 @@ use Aws\Handler\GuzzleV6\GuzzleHandler; use GuzzleHttp\Client; use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\HandlerStack; use GuzzleHttp\Promise; @@ -319,6 +320,39 @@ private function getClientWithHeaderMiddleware($expectedValue) return new GuzzleHandler($baseClient); } + public function testResolveCredentialsWithAccountId() + { + $testAccountId = 'foo'; + $expiration = time() + 1000; + $testHandler = function (RequestInterface $_) use ($expiration, $testAccountId) { + $jsonResponse = << $testHandler + ]); + try { + /** @var Credentials $credentials */ + $credentials = $provider()->wait(); + $this->assertSame('foo', $credentials->getAccessKeyId()); + $this->assertSame('foo', $credentials->getSecretKey()); + $this->assertSame('bazz', $credentials->getSecurityToken()); + $this->assertSame($expiration, $credentials->getExpiration()); + $this->assertSame($testAccountId, $credentials->getAccountId()); + } catch (GuzzleException $e) { + self::fail($e->getMessage()); + } + + } + /** * @dataProvider successTestCases * diff --git a/tests/Credentials/InstanceProfileProviderTest.php b/tests/Credentials/InstanceProfileProviderTest.php index c39b8588ff..7324690feb 100644 --- a/tests/Credentials/InstanceProfileProviderTest.php +++ b/tests/Credentials/InstanceProfileProviderTest.php @@ -1626,4 +1626,52 @@ private function getClientForEndpointTesting(\Closure $assertingFunction): \Clos return Promise\Create::rejectionFor(['exception' => new \Exception('Unexpected error!')]); }; } + + public function testResolveCredentialsWithAccountId() + { + $testAccountId = 'foo'; + $expiration = time() + 1000; + $testHandler = function (RequestInterface $request) use ($expiration, $testAccountId) { + if ($request->getMethod() === 'PUT' && $request->getUri()->getPath() === '/latest/api/token') { + return Promise\Create::promiseFor(new Response(200, [], Psr7\Utils::streamFor(''))); + } elseif ($request->getMethod() === 'GET') { + switch ($request->getUri()->getPath()) { + case '/latest/meta-data/iam/security-credentials/': + return Promise\Create::promiseFor(new Response(200, [], Psr7\Utils::streamFor('MockProfile'))); + case '/latest/meta-data/iam/security-credentials/MockProfile': + $jsonResponse = << new \Exception('Unexpected error!')]); + }; + $provider = new InstanceProfileProvider([ + 'client' => $testHandler + ]); + /** @var Credentials $credentials */ + $credentials = $provider()->wait(); + $this->assertSame('foo', $credentials->getAccessKeyId()); + $this->assertSame('foo', $credentials->getSecretKey()); + $this->assertSame('bazz', $credentials->getSecurityToken()); + $this->assertSame($expiration, $credentials->getExpiration()); + $this->assertSame($testAccountId, $credentials->getAccountId()); + } } diff --git a/tests/EndpointV2/EndpointProviderV2Test.php b/tests/EndpointV2/EndpointProviderV2Test.php index 196b62ce65..09e5b7fcf4 100644 --- a/tests/EndpointV2/EndpointProviderV2Test.php +++ b/tests/EndpointV2/EndpointProviderV2Test.php @@ -2,6 +2,7 @@ namespace Aws\Test\EndpointV2; use Aws\Auth\Exception\UnresolvedAuthSchemeException; +use Aws\Credentials\Credentials; use Aws\EndpointV2\EndpointDefinitionProvider; use Aws\EndpointV2\EndpointProviderV2; use Aws\EndpointV2\Ruleset\Ruleset; @@ -10,6 +11,7 @@ use Aws\Exception\UnresolvedEndpointException; use Aws\Middleware; use Aws\Test\UsesServiceTrait; +use GuzzleHttp\Promise\Create; use GuzzleHttp\Psr7\Uri; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -160,12 +162,11 @@ public function testServiceEndpointAndErrorCases( } } - public function rulesetProtocolEndpointAndErrorCaseProvider() + public function rulesetProtocolEndpointAndErrorCaseProvider(): \Generator { - $protocolTestCases = []; $serviceList = \Aws\manifest(); - forEach($serviceList as $service => $serviceValue) { + forEach($serviceList as $service => $serviceValue) { $testFile = EndpointDefinitionProvider::getEndpointTests($service, 'latest'); foreach($testFile['testCases'] as $case) { @@ -186,24 +187,38 @@ public function rulesetProtocolEndpointAndErrorCaseProvider() } $clientArgs = [ - 'region' => $builtInParams['AWS::Region'], - 'endpoint' => isset($builtInParams['SDK::Endpoint']) ? $builtInParams['SDK::Endpoint'] : null, - 'use_fips_endpoint' => isset($builtInParams['AWS::UseFIPS']) ? $builtInParams['AWS::UseFIPS'] : null, - 'use_dual_stack_endpoint' => isset($builtInParams['AWS::UseDualStack']) ? $builtInParams['AWS::UseDualStack'] : null, - 's3_us_east_1_regional_endpoint' => isset($builtInParams['AWS::S3::UseGlobalEndpoint']) ? $builtInParams['AWS::S3::UseGlobalEndpoint'] === true ? 'legacy' : 'regional' : null, - 'sts_regional_endpoints' => isset($builtInParams['AWS::STS::UseGlobalEndpoint']) ? $builtInParams['AWS::STS::UseGlobalEndpoint'] === true ? 'legacy' : 'regional' : null, - 'use_accelerate_endpoint' => isset($builtInParams['AWS::S3::Accelerate']) ? $builtInParams['AWS::S3::Accelerate'] : null, - 'use_path_style_endpoint' => isset($builtInParams['AWS::S3::ForcePathStyle']) ? $builtInParams['AWS::S3::ForcePathStyle'] : null, - 'use_arn_region' => isset($useArnRegion) ? $useArnRegion : null, - 'disable_multiregion_access_points' => isset($builtInParams['AWS::S3::DisableMultiRegionAccessPoints']) ? $builtInParams['AWS::S3::DisableMultiRegionAccessPoints'] : null + 'region' => $builtInParams['AWS::Region'] ?? 'us-east-1', + 'endpoint' => $builtInParams['SDK::Endpoint'] ?? null, + 'use_fips_endpoint' => $builtInParams['AWS::UseFIPS'] ?? null, + 'use_dual_stack_endpoint' => $builtInParams['AWS::UseDualStack'] ?? null, + 's3_us_east_1_regional_endpoint' => isset($builtInParams['AWS::S3::UseGlobalEndpoint']) ? ($builtInParams['AWS::S3::UseGlobalEndpoint'] === true ? 'legacy' : 'regional') : null, + 'sts_regional_endpoints' => isset($builtInParams['AWS::STS::UseGlobalEndpoint']) ? ($builtInParams['AWS::STS::UseGlobalEndpoint'] === true ? 'legacy' : 'regional') : null, + 'use_accelerate_endpoint' => $builtInParams['AWS::S3::Accelerate'] ?? null, + 'use_path_style_endpoint' => $builtInParams['AWS::S3::ForcePathStyle'] ?? null, + 'use_arn_region' => $useArnRegion ?? null, + 'disable_multiregion_access_points' => $builtInParams['AWS::S3::DisableMultiRegionAccessPoints'] ?? null, + 'account_id_endpoint_mode' => $builtInParams['AWS::Auth::AccountIdEndpointMode'] ?? null ]; - array_push($caseArgs, $clientArgs, $operationInput, $case['expect'], isset($case['expect']['error'])); - $protocolTestCases[] = $caseArgs; - } + if (isset($builtInParams['AWS::Auth::AccountId'])) { + $clientArgs['credentials'] = [ + 'key' => 'foo', + 'secret' => 'foo', + 'token' => 'foo', + 'expires' => null, + 'accountId' => $builtInParams['AWS::Auth::AccountId'] + ]; + } + yield [ + $service, + $clientArgs, + $operationInput, + $case['expect'], + isset($case['expect']['error']) + ]; + } } } - return $protocolTestCases; } /** @@ -218,21 +233,18 @@ public function testRulesetProtocolEndpointAndErrorCases($service, $clientArgs, if ($errorCase) { $this->expectException(UnresolvedEndpointException::class); $this->expectExceptionMessage($expected['error']); - goto clientInstantiation; - } - - //accounts for legacy global endpoint behavior - if (strpos($expected['endpoint']['url'], 's3.us-east-1.amazonaws.com') !== false - && $clientArgs['s3_us_east_1_regional_endpoint'] !== true - ) { - $this->markTestSkipped(); - } - if ($service == 's3') { - $clientArgs['disable_express_session_auth'] = true; + } else { + //accounts for legacy global endpoint behavior + if (strpos($expected['endpoint']['url'], 's3.us-east-1.amazonaws.com') !== false + && $clientArgs['s3_us_east_1_regional_endpoint'] !== true + ) { + $this->markTestSkipped(); + } + if ($service == 's3') { + $clientArgs['disable_express_session_auth'] = true; + } } - clientInstantiation: - $client = $this->getTestClient($service, $clientArgs); $this->addMockResults($client, [[]]); $command = $client->getCommand( @@ -241,73 +253,70 @@ public function testRulesetProtocolEndpointAndErrorCases($service, $clientArgs, ); $list = $client->getHandlerList(); - if ($errorCase) { - goto resolveHandler; - } - - $list->appendSign(Middleware::tap(function($cmd, $req) use ($service, $expected) { - $expectedEndpoint = $expected['endpoint']; - $expectedUri = new Uri($expected['endpoint']['url']); - $this->assertStringContainsString( - $expectedUri->getHost(), - $req->getUri()->getHost() - ); - - if (isset($expectedEndpoint['properties']['authSchemes'])) { - $expectedAuthScheme = null; - foreach ($expectedEndpoint['properties']['authSchemes'] as $authScheme) { - // Skip sigv4a if awscrt extension is not loaded - if ($authScheme['name'] === 'sigv4a' && !extension_loaded('awscrt')) { - continue; + if (!$errorCase) { + $list->appendSign(Middleware::tap(function($cmd, $req) use ($service, $expected) { + $expectedEndpoint = $expected['endpoint']; + $expectedUri = new Uri($expected['endpoint']['url']); + $this->assertStringContainsString( + $expectedUri->getHost(), + $req->getUri()->getHost() + ); + + if (isset($expectedEndpoint['properties']['authSchemes'])) { + $expectedAuthScheme = null; + foreach ($expectedEndpoint['properties']['authSchemes'] as $authScheme) { + // Skip sigv4a if awscrt extension is not loaded + if ($authScheme['name'] === 'sigv4a' && !extension_loaded('awscrt')) { + continue; + } + + $expectedAuthScheme = $authScheme; + break; } - $expectedAuthScheme = $authScheme; - break; - } - - if ($expectedAuthScheme) { - if ((isset($expectedAuthScheme['disableDoubleEncoding']) - && $expectedAuthScheme['disableDoubleEncoding'] === true) - && $expectedAuthScheme['name'] !== 'sigv4a' - ) { - $expectedVersion = 's3v4'; - } else { - $expectedVersion = str_replace('sig', '', $expectedAuthScheme['name']); - } - $this->assertEquals( - $cmd['@context']['signature_version'], - $expectedVersion - ); - $this->assertEquals( - $cmd['@context']['signing_service'], - $expectedAuthScheme['signingName'] - ); - if (isset($cmd['@context']['signing_region'])) { + if ($expectedAuthScheme) { + if ((isset($expectedAuthScheme['disableDoubleEncoding']) + && $expectedAuthScheme['disableDoubleEncoding'] === true) + && $expectedAuthScheme['name'] !== 'sigv4a' + ) { + $expectedVersion = 's3v4'; + } else { + $expectedVersion = str_replace('sig', '', $expectedAuthScheme['name']); + } $this->assertEquals( - $cmd['@context']['signing_region'], - $expectedAuthScheme['signingRegion'] + $cmd['@context']['signature_version'], + $expectedVersion ); - } elseif (isset($cmd['@context']['signing_region_set'])) { $this->assertEquals( - $cmd['@context']['signing_region_set'], - $expectedAuthScheme['signingRegionSet']); + $cmd['@context']['signing_service'], + $expectedAuthScheme['signingName'] + ); + if (isset($cmd['@context']['signing_region'])) { + $this->assertEquals( + $cmd['@context']['signing_region'], + $expectedAuthScheme['signingRegion'] + ); + } elseif (isset($cmd['@context']['signing_region_set'])) { + $this->assertEquals( + $cmd['@context']['signing_region_set'], + $expectedAuthScheme['signingRegionSet']); + } } } - } - if (isset($expectedEndpoint['headers'])) { - $expectedHeaders = $expectedEndpoint['headers']; - $returnedHeaders = $req->getHeaders(); - - foreach($expectedHeaders as $headerKey => $headerValue) { - $this->assertArrayHasKey($headerKey, $returnedHeaders); - $this->assertEquals( - $headerValue[0], - $returnedHeaders[$headerKey][0] - ); + if (isset($expectedEndpoint['headers'])) { + $expectedHeaders = $expectedEndpoint['headers']; + $returnedHeaders = $req->getHeaders(); + + foreach($expectedHeaders as $headerKey => $headerValue) { + $this->assertArrayHasKey($headerKey, $returnedHeaders); + $this->assertEquals( + $headerValue[0], + $returnedHeaders[$headerKey][0] + ); + } } - } - })); - resolveHandler: + })); + } $handler = $list->resolve(); try { diff --git a/tests/Sts/StsClientTest.php b/tests/Sts/StsClientTest.php index db48a2b0eb..dfccb321d1 100644 --- a/tests/Sts/StsClientTest.php +++ b/tests/Sts/StsClientTest.php @@ -2,12 +2,16 @@ namespace Aws\Test\Sts; use Aws\Api\DateTimeResult; +use Aws\Credentials\CredentialProvider; +use Aws\Credentials\Credentials; use Aws\Credentials\CredentialsInterface; use Aws\Endpoint\PartitionEndpointProvider; +use Aws\Exception\CredentialsException; use Aws\LruArrayCache; use Aws\Result; use Aws\Sts\RegionalEndpoints\Configuration; use Aws\Sts\StsClient; +use GuzzleHttp\Promise\Create; use GuzzleHttp\Psr7\Uri; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -16,6 +20,17 @@ */ class StsClientTest extends TestCase { + private $deferredFns = []; + + public function tearDown(): void + { + foreach ($this->deferredFns as $deferredFn) { + $deferredFn(); + } + + $this->deferredFns = []; + } + public function testCanCreateCredentialsObjectFromStsResult() { $result = new Result([ @@ -84,4 +99,362 @@ public function testAddsStsRegionalEndpointsCacheArgument() $this->assertSame($uri->getHost(), $client->getEndpoint()->getHost()); } + + public function testCanCreateCredentialsObjectFromStsResultWithAssumedRoleUser() + { + $testAccountId = '123456789012'; + $result = new Result([ + 'AssumedRoleUser' => [ + 'Arn' => "arn:aws:iam::$testAccountId:user/test-user-1" + ], + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'bar', + 'SessionToken' => 'baz', + 'Expiration' => DateTimeResult::fromEpoch(time() + 10) + ] + ]); + + $client = new StsClient(['region' => 'us-east-1', 'version' => 'latest']); + $credentials = $client->createCredentials($result); + $this->assertInstanceOf( + CredentialsInterface::class, + $credentials + ); + $this->assertSame('foo', $credentials->getAccessKeyId()); + $this->assertSame('bar', $credentials->getSecretKey()); + $this->assertSame('baz', $credentials->getSecurityToken()); + $this->assertSame($testAccountId, $credentials->getAccountId()); + $this->assertIsInt($credentials->getExpiration()); + $this->assertFalse($credentials->isExpired()); + } + + public function testCanCreateCredentialsObjectFromStsResultWithFederatedUser() + { + $result = new Result([ + 'FederatedUser' => [ + 'Arn' => 'arn:aws:iam::123456789000:user/test-user-1' + ], + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'bar', + 'SessionToken' => 'baz', + 'Expiration' => DateTimeResult::fromEpoch(time() + 10) + ] + ]); + + $client = new StsClient(['region' => 'us-east-1', 'version' => 'latest']); + $credentials = $client->createCredentials($result); + $this->assertInstanceOf( + CredentialsInterface::class, + $credentials + ); + $this->assertSame('foo', $credentials->getAccessKeyId()); + $this->assertSame('bar', $credentials->getSecretKey()); + $this->assertSame('baz', $credentials->getSecurityToken()); + $this->assertSame('123456789000', $credentials->getAccountId()); + $this->assertIsInt($credentials->getExpiration()); + $this->assertFalse($credentials->isExpired()); + } + + /** + * @dataProvider stsAssumeRoleOperationsDataProvider + * + * @return void + */ + public function testStsAssumeRoleOperationsWithAccountId($response, $expected) + { + $operation = 'assumeRole'; + $stsClient = $this->getTestStsClient($operation, $response); + $params = [ + 'client' => $stsClient, + 'assume_role_params' => [ + 'RoleArn' => 'arn:aws:sts::123456789012:assumed-role/test-role/Name', + 'RoleSessionName' => 'TestSession' + ] + ]; + $provider = CredentialProvider::assumeRole($params); + $response = $provider()->wait(); + $expected = $this->normalizeExpectedResponse($expected); + + self::assertSame($expected->toArray(), $response->toArray()); + } + + public function stsAssumeRoleOperationsDataProvider(): array + { + return [ + 'Sts::AssumeRole' => [ + 'response' => [ + "AssumedRoleUser" => [ + "AssumedRoleId" => "roleId", + "Arn" => "arn:aws:sts::123456789012:assumed-role/assume-role-integration-test-role/Name" + ], + "Credentials" => [ + "AccessKeyId" => "foo", + "SecretAccessKey" => "bar", + "SessionToken" => "baz" + ] + ], + 'expected' => [ + "accountId" => "123456789012", + "accessKeyId" => "foo", + "secretAccessKey" => "bar", + "sessionToken" => "baz" + ] + ] + ]; + } + + /** + * @dataProvider stsAssumeRoleWithSAMLOperationsDataProvider + * + * @return void + */ + public function testStsAssumeRoleWithSAMLOperationsWithAccountId($response, $expected) + { + $operation = 'assumeRoleWithSAML'; + $stsClient = $this->getTestStsClient($operation, $response); + $provider = function () use($stsClient) { + $params = [ + 'RoleArn' => 'arn:aws:sts::123456789012:assumed-role/test-role/Name', + 'PrincipalArn' => 'arn:aws:sts::123456789012:assumed-role/test-role/Name', + 'SAMLAssertion' => 'VGhpcyBpcyBhIHRlc3QgYXNzZXJ0aW9u' + ]; + + return $stsClient->assumeRoleWithSAMLAsync($params) + -> then(function (Result $result) use ($stsClient) { + return $stsClient->createCredentials($result); + }) -> otherwise(function (\RuntimeException $exception) { + throw new CredentialsException( + "Error in retrieving assume role credentials.", + 0, + $exception + ); + }); + }; + $response = $provider()->wait(); + $expected = $this->normalizeExpectedResponse($expected); + + self::assertSame($expected->toArray(), $response->toArray()); + } + + public function stsAssumeRoleWithSAMLOperationsDataProvider(): array + { + return [ + 'Sts::AssumeRoleWithSaml' => [ + 'response' => [ + "AssumedRoleUser" => [ + "AssumedRoleId" => "roleId", + "Arn" => "arn:aws:sts::123456789012:assumed-role/assume-role-integration-test-role/Name" + ], + "Credentials" => [ + "AccessKeyId" => "foo", + "SecretAccessKey" => "bar", + "SessionToken" => "baz" + ] + ], + 'expected' => [ + "accountId" => "123456789012", + "accessKeyId" => "foo", + "secretAccessKey" => "bar", + "sessionToken" => "baz" + ] + ] + ]; + } + + /** + * @dataProvider stsAssumeRoleWithWebIdentityOperationsDataProvider + * + * @return void + */ + public function testStsAssumeRoleWithWebIdentityOperationsWithAccountId($response, $expected) + { + $operation = 'assumeRoleWithWebIdentity'; + $stsClient = $this->getTestStsClient($operation, $response); + $tokenPath = $this->createTestWebIdentityToken(); + $this->putEnv([ + CredentialProvider::ENV_ARN => 'arn:aws:sts::123456789012:assumed-role/test-role/Name', + CredentialProvider::ENV_ROLE_SESSION_NAME => 'TestSession', + CredentialProvider::ENV_TOKEN_FILE => $tokenPath + ]); + $params = [ + 'stsClient' => $stsClient, + 'region' => 'us-east-1' + ]; + $provider = CredentialProvider::assumeRoleWithWebIdentityCredentialProvider($params); + $response = $provider()->wait(); + $expected = $this->normalizeExpectedResponse($expected); + + self::assertSame($expected->toArray(), $response->toArray()); + } + + public function stsAssumeRoleWithWebIdentityOperationsDataProvider(): array + { + return [ + 'Sts::AssumeRoleWithWebIdentity' => [ + 'response' => [ + "AssumedRoleUser" => [ + "AssumedRoleId" => "roleId", + "Arn" => "arn:aws:sts::123456789012:assumed-role/assume-role-integration-test-role/Name" + ], + "Credentials" => [ + "AccessKeyId" => "foo", + "SecretAccessKey" => "bar", + "SessionToken" => "baz" + ] + ], + 'expected' => [ + "accountId" => "123456789012", + "accessKeyId" => "foo", + "secretAccessKey" => "bar", + "sessionToken" => "baz" + ] + ] + ]; + } + + /** + * @dataProvider stsGetFederationTokenOperationsDataProvider + * + * @return void + */ + public function testStsGetFederationTokenOperationsWithAccountId($response, $expected) + { + $operation = 'getFederationToken'; + $stsClient = $this->getTestStsClient($operation, $response); + $provider = function () use ($stsClient) { + $params = [ + 'Name' => 'TestUserName' + ]; + + return $stsClient->getFederationTokenAsync($params) + -> then(function (Result $result) use ($stsClient) { + return $stsClient->createCredentials($result); + }) -> otherwise(function (\RuntimeException $exception) { + throw new CredentialsException( + "Error in retrieving assume role credentials.", + 0, + $exception + ); + }); + }; + $response = $provider()->wait(); + $expected = $this->normalizeExpectedResponse($expected); + + self::assertSame($expected->toArray(), $response->toArray()); + } + + public function stsGetFederationTokenOperationsDataProvider(): array + { + return [ + 'Sts::GetFederationToken' => [ + 'response' => [ + "FederatedUser" => [ + "FederatedUserId" => "roleId", + "Arn" => "arn:aws:sts::123456789012:assumed-role/assume-role-integration-test-role/Name" + ], + "Credentials" => [ + "AccessKeyId" => "foo", + "SecretAccessKey" => "bar", + "SessionToken" => "baz" + ] + ], + 'expected' => [ + "accountId" => "123456789012", + "accessKeyId" => "foo", + "secretAccessKey" => "bar", + "sessionToken" => "baz" + ] + ], + ]; + } + + private function getTestStsClient($operation, $response) + { + $stsClient = $this->getMockBuilder(StsClient::class) + -> disableOriginalConstructor() + -> setMethods(['__call']) + -> getMock(); + $stsClient->method('__call') + -> willReturnCallback(function ($callOperation) use ($operation, $response) { + if (($callOperation === $operation . 'Async')) { + return Create::promiseFor( + new Result( + $response + ) + ); + } + + if (($callOperation === $operation)) { + return new Result( + $response + ); + } + + return null; + }); + + return $stsClient; + } + + /** + * This method normalize an expected response, supplied by data providers, + * into a valid credentials object. + * + * @param array $expectedResponse the expected response to normalize. + * + * @return Credentials + */ + private function normalizeExpectedResponse(array $expectedResponse): Credentials + { + return new Credentials( + $expectedResponse['accessKeyId'] ?? null, + $expectedResponse['secretAccessKey'] ?? null, + $expectedResponse['sessionToken'] ?? null, + $expectedResponse['expires'] ?? null, + $expectedResponse['accountId'] ?? null + ); + } + + /** + * This method is designed for setting environment variables. It takes an array of key-value + * pairs where the keys represent the environment variable names and the values represent + * their corresponding values. + * + * @param array $envValues + * @return void + */ + private function putEnv(array $envValues): void + { + foreach ($envValues as $key => $value) { + $currentValue = getenv($key); + $deferFn = function () use ($key, $currentValue) { + if (!empty($currentValue)) { + putenv($key.'='.$currentValue); + } + }; + $this->deferredFns[] = $deferFn; + + putenv($key.'='.$value); + } + } + + /** + * This method creates a test token file at a temporary location. + * + * @return string the path for the test token file created. + */ + private function createTestWebIdentityToken(): string + { + $dir = sys_get_temp_dir(); + $tokenPath = $dir . '/token'; + file_put_contents($tokenPath, 'token'); + $deferFn = function () use ($tokenPath) { + unlink($tokenPath); + }; + $this->deferredFns[] = $deferFn; + + return $tokenPath; + } }