Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions src/Providers/EcsRamRoleCredentialsProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Request\Request;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;
Expand Down Expand Up @@ -110,6 +111,16 @@ private function filterDisableECSIMDSv1($params)
}
}

/**
* @param string|null $metadataToken
*
* @return bool
*/
private function shouldFallbackToIMDSv1($metadataToken)
{
return !is_null($metadataToken) && !$this->disableIMDSv1;
}

/**
* Get credentials by request.
*
Expand All @@ -128,12 +139,33 @@ public function refreshCredentials()
$this->roleName = $this->getRoleNameFromMeta();
}

$url = $this->metadataHost . $this->ecsUri . $this->roleName;
$metadataToken = $this->getMetadataToken();
try {
return $this->doRefreshCredentials($this->roleName, $metadataToken);
} catch (Exception $e) {
if ($this->shouldFallbackToIMDSv1($metadataToken)) {
return $this->doRefreshCredentials($this->roleName, null);
}
throw $e;
}
}

/**
* @param string $roleName
* @param string|null $metadataToken
*
* @return RefreshResult
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws GuzzleException
*/
private function doRefreshCredentials($roleName, $metadataToken)
{
$url = $this->metadataHost . $this->ecsUri . $roleName;
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;

$metadataToken = $this->getMetadataToken();
if (!is_null($metadataToken)) {
$options['headers']['X-aliyun-ecs-metadata-token'] = $metadataToken;
}
Expand Down Expand Up @@ -174,12 +206,32 @@ public function refreshCredentials()
* @throws GuzzleException
*/
private function getRoleNameFromMeta()
{
$metadataToken = $this->getMetadataToken();
try {
return $this->doGetRoleNameFromMeta($metadataToken);
} catch (Exception $e) {
if ($this->shouldFallbackToIMDSv1($metadataToken)) {
return $this->doGetRoleNameFromMeta(null);
}
throw $e;
}
}

/**
* @param string|null $metadataToken
*
* @return string
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws GuzzleException
*/
private function doGetRoleNameFromMeta($metadataToken)
{
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;

$metadataToken = $this->getMetadataToken();
if (!is_null($metadataToken)) {
$options['headers']['X-aliyun-ecs-metadata-token'] = $metadataToken;
}
Expand Down Expand Up @@ -209,7 +261,7 @@ private function getRoleNameFromMeta()
/**
* Get metadata token by request.
*
* @return string
* @return string|null
* @throws RuntimeException
* @throws GuzzleException
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Utils/MockTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public static function cancelMock()
{
self::$mockQueue = [];
self::$mock = null;
// Clear in place so history middleware references stay valid.
array_splice(self::$history, 0);
}

/**
Expand Down
18 changes: 16 additions & 2 deletions tests/Unit/EcsRamRoleCredentialTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public function testDefault404()
{
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(404, [], 'RoleName');
// IMDSv1 fallback retry
Credentials::mockResponse(404, [], 'RoleName');

$this->credential = new EcsRamRoleCredential();

Expand All @@ -161,6 +163,8 @@ public function testDefault500()
{
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(500, [], 'RoleName');
// IMDSv1 fallback retry
Credentials::mockResponse(500, [], 'RoleName');
$this->credential = new EcsRamRoleCredential();

$this->expectException(RuntimeException::class);
Expand All @@ -185,6 +189,8 @@ public function testDefaultEmpty()
Credentials::mockResponse(200, [], 'RoleNameTest');
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(200, [], []);
// IMDSv1 fallback retry after incomplete credentials body
Credentials::mockResponse(200, [], []);

$this->credential = new EcsRamRoleCredential();

Expand Down Expand Up @@ -237,6 +243,8 @@ public function testStsIncomplete()
$credential = new EcsRamRoleCredential('EcsRamRoleTest2');
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(200, [], $result);
// IMDSv1 fallback retry after incomplete credentials body
Credentials::mockResponse(200, [], $result);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Error retrieving credentials from IMDS result:{"Expiration":"2049-10-01 00:00:00","AccessKeyId":"foo"}');
// Test
Expand All @@ -260,6 +268,8 @@ public function testStsNoCode()
$credential = new EcsRamRoleCredential('EcsRamRoleTest2');
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(200, [], $result);
// IMDSv1 fallback retry after Code missing
Credentials::mockResponse(200, [], $result);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Error retrieving credentials from IMDS result, Code is not Success:{"Expiration":"2049-10-01 00:00:00","AccessKeyId":"foo","AccessKeySecret":"bar","SecurityToken":"token"}');
// Test
Expand All @@ -281,6 +291,8 @@ public function testSts404()
$credential = new EcsRamRoleCredential('EcsRamRoleTest3');
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(404, [], $result);
// IMDSv1 fallback retry
Credentials::mockResponse(404, [], $result);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The role was not found in the instance');
Expand All @@ -304,6 +316,8 @@ public function testSts500()
$credential = new EcsRamRoleCredential('EcsRamRoleTest3');
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(500, [], $result);
// IMDSv1 fallback retry
Credentials::mockResponse(500, [], $result);

$this->expectException(RuntimeException::class);
if (method_exists($this, 'expectExceptionMessageMatches')) {
Expand Down Expand Up @@ -345,9 +359,9 @@ public function testStsWithoutMock()

$this->expectException(RuntimeException::class);
if (method_exists($this, 'expectExceptionMessageMatches')) {
$this->expectExceptionMessageMatches('/Timeout was reached/');
$this->expectExceptionMessageMatches('/Timeout was reached|Connection timed out|cURL error 28/');
} elseif (method_exists($this, 'expectExceptionMessageRegExp')) {
$this->expectExceptionMessageRegExp('/Timeout was reached/');
$this->expectExceptionMessageRegExp('/Timeout was reached|Connection timed out|cURL error 28/');
}
// Test
self::assertEquals('foo', $credential->getAccessKeyId());
Expand Down
111 changes: 103 additions & 8 deletions tests/Unit/Providers/EcsRamRoleCredentialsProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use AlibabaCloud\Credentials\Credentials;
use AlibabaCloud\Credentials\Providers\EcsRamRoleCredentialsProvider;
use AlibabaCloud\Credentials\Providers\SessionCredentialsProvider;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\TestCase;
Expand All @@ -25,6 +26,18 @@ protected function initialize()
{
parent::setUp();
Credentials::cancelMock();
putenv('ALIBABA_CLOUD_ECS_METADATA_DISABLED');
putenv('ALIBABA_CLOUD_IMDSV1_DISABLED');
putenv('ALIBABA_CLOUD_ECS_METADATA');
$this->clearCredentialsCache();
}

private function clearCredentialsCache()
{
$reflection = new ReflectionClass(SessionCredentialsProvider::class);
$property = $reflection->getProperty('credentialsCache');
$property->setAccessible(true);
$property->setValue(null, []);
}

private function getPrivateField($instance, $field)
Expand Down Expand Up @@ -91,15 +104,17 @@ public function testEnvDisabled()
putenv("ALIBABA_CLOUD_ECS_METADATA_DISABLED=true");
$provider = new EcsRamRoleCredentialsProvider([], []);

$this->expectException(RuntimeException::class);
if (method_exists($this, 'expectExceptionMessageMatches')) {
$this->expectExceptionMessageMatches('/IMDS credentials is disabled/');
} elseif (method_exists($this, 'expectExceptionMessageRegExp')) {
$this->expectExceptionMessageRegExp('/IMDS credentials is disabled/');
try {
$this->expectException(RuntimeException::class);
if (method_exists($this, 'expectExceptionMessageMatches')) {
$this->expectExceptionMessageMatches('/IMDS credentials is disabled/');
} elseif (method_exists($this, 'expectExceptionMessageRegExp')) {
$this->expectExceptionMessageRegExp('/IMDS credentials is disabled/');
}
$provider->getCredentials();
} finally {
putenv('ALIBABA_CLOUD_ECS_METADATA_DISABLED');
}
$provider->getCredentials();

putenv("ALIBABA_CLOUD_ECS_METADATA_DISABLED=");
}

public function testGetDisableECSIMDSv1()
Expand Down Expand Up @@ -227,4 +242,84 @@ public function testEnableV1404()
$request = end($histroy)['request'];
self::assertEquals(null, $token);
}

public function testFallbackToIMDSv1WhenCredentialGetFailsAfterToken()
{
$result = [
'Expiration' => '2049-10-01 00:00:00',
'AccessKeyId' => 'foo',
'AccessKeySecret' => 'bar',
'SecurityToken' => 'token',
'Code' => 'Success',
];
$provider = new EcsRamRoleCredentialsProvider([
'roleName' => 'test',
'disableIMDSv1' => false,
]);

Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(500, [], 'v2 failed');
Credentials::mockResponse(200, [], $result);

$credential = $provider->getCredentials();
self::assertEquals('foo', $credential->getAccessKeyId());
self::assertEquals('bar', $credential->getAccessKeySecret());
self::assertEquals('token', $credential->getSecurityToken());

$history = Credentials::getHistroy();
self::assertEquals(3, count($history));
self::assertEquals('PUT', $history[0]['request']->getMethod());
self::assertTrue($history[1]['request']->hasHeader('X-aliyun-ecs-metadata-token'));
self::assertFalse($history[2]['request']->hasHeader('X-aliyun-ecs-metadata-token'));
}

public function testNoFallbackWhenDisableIMDSv1True()
{
$provider = new EcsRamRoleCredentialsProvider([
'roleName' => 'test',
'disableIMDSv1' => true,
]);

Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(500, [], 'v2 failed');

$this->expectException(RuntimeException::class);
if (method_exists($this, 'expectExceptionMessageMatches')) {
$this->expectExceptionMessageMatches('/Error refreshing credentials from IMDS, statusCode: 500/');
} elseif (method_exists($this, 'expectExceptionMessageRegExp')) {
$this->expectExceptionMessageRegExp('/Error refreshing credentials from IMDS, statusCode: 500/');
}

$provider->getCredentials();
}

public function testFallbackToIMDSv1WhenRoleNameGetFailsAfterToken()
{
$result = [
'Expiration' => '2049-10-01 00:00:00',
'AccessKeyId' => 'foo',
'AccessKeySecret' => 'bar',
'SecurityToken' => 'token',
'Code' => 'Success',
];
$provider = new EcsRamRoleCredentialsProvider([
'disableIMDSv1' => false,
]);

// getRoleNameFromMeta: token ok, GET with token fails, retry without token ok
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(404, [], 'not found');
Credentials::mockResponse(200, [], 'fallback-role');
// refreshCredentials: token + credentials
Credentials::mockResponse(200, [], 'Token');
Credentials::mockResponse(200, [], $result);

$credential = $provider->getCredentials();
self::assertEquals('foo', $credential->getAccessKeyId());
self::assertEquals('fallback-role', $provider->getRoleName());

$history = Credentials::getHistroy();
self::assertEquals(5, count($history));
self::assertFalse($history[2]['request']->hasHeader('X-aliyun-ecs-metadata-token'));
}
}
Loading