From c77d08ed0a378e831fdb78182034704ed51fb06a Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Tue, 5 Nov 2024 08:58:44 -0800 Subject: [PATCH] feat: user agent v2.1 second revision - Move the user agent middleware after the signing step in order to gather signature metrics. - Add request compression metric gathering. - Add specific testing for signatures. - Add specific testing for request compression. - Add specific testing for s3 encryption clients. - Add credentials metric gathering logic. - Add tests for credentials metrics. - Make existent credentials tests to work with the new field `source`. For example, for tests around profile credentials the source property for credentials MUST be `profile`. The default value for this field is `static`. --- src/AwsClient.php | 47 +- src/Command.php | 9 +- .../AssumeRoleCredentialProvider.php | 5 +- ...eRoleWithWebIdentityCredentialProvider.php | 24 +- src/Credentials/CredentialProvider.php | 26 +- src/Credentials/CredentialSources.php | 22 + src/Credentials/Credentials.php | 23 +- src/Credentials/EcsCredentialProvider.php | 3 +- src/Credentials/InstanceProfileProvider.php | 3 +- src/EndpointV2/EndpointV2Middleware.php | 32 +- src/MetricsBuilder.php | 203 ++- src/Middleware.php | 6 + src/RequestCompressionMiddleware.php | 8 +- src/S3/ApplyChecksumMiddleware.php | 43 +- src/Sts/StsClient.php | 5 +- src/UserAgentMiddleware.php | 50 +- tests/AwsClientTest.php | 19 - tests/Credentials/CredentialProviderTest.php | 31 +- tests/Credentials/CredentialsTest.php | 10 +- tests/MetricsBuilderTestTrait.php | 2 +- tests/Sts/StsClientTest.php | 32 +- tests/UserAgentMiddlewareTest.php | 1232 ++++++++++++++++- 22 files changed, 1644 insertions(+), 191 deletions(-) create mode 100644 src/Credentials/CredentialSources.php diff --git a/src/AwsClient.php b/src/AwsClient.php index d63830cae2..63f67f61b7 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -280,10 +280,6 @@ public function __construct(array $args) if (isset($args['with_resolved'])) { $args['with_resolved']($config); } - MetricsBuilder::appendMetricsCaptureMiddleware( - $this->getHandlerList(), - MetricsBuilder::RESOURCE_MODEL - ); $this->addUserAgentMiddleware($config); } @@ -453,7 +449,7 @@ private function addSignatureMiddleware(array $args) } $resolver = static function ( - CommandInterface $c + CommandInterface $command ) use ( $api, $provider, @@ -465,17 +461,17 @@ private function addSignatureMiddleware(array $args) $handlerList ) { if (!$configuredSignatureVersion) { - if (!empty($c['@context']['signing_region'])) { - $region = $c['@context']['signing_region']; + if (!empty($command['@context']['signing_region'])) { + $region = $command['@context']['signing_region']; } - if (!empty($c['@context']['signing_service'])) { - $name = $c['@context']['signing_service']; + if (!empty($command['@context']['signing_service'])) { + $name = $command['@context']['signing_service']; } - if (!empty($c['@context']['signature_version'])) { - $signatureVersion = $c['@context']['signature_version']; + if (!empty($command['@context']['signature_version'])) { + $signatureVersion = $command['@context']['signature_version']; } - $authType = $api->getOperation($c->getName())['authtype']; + $authType = $api->getOperation($command->getName())['authtype']; switch ($authType){ case 'none': $signatureVersion = 'anonymous'; @@ -490,20 +486,21 @@ private function addSignatureMiddleware(array $args) } if ($signatureVersion === 'v4a') { - $commandSigningRegionSet = !empty($c['@context']['signing_region_set']) - ? implode(', ', $c['@context']['signing_region_set']) + $commandSigningRegionSet = !empty($command['@context']['signing_region_set']) + ? implode(', ', $command['@context']['signing_region_set']) : null; $region = $signingRegionSet ?? $commandSigningRegionSet ?? $region; - - MetricsBuilder::appendMetricsCaptureMiddleware( - $handlerList, - MetricsBuilder::SIGV4A_SIGNING - ); } + // Capture signature metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'signature', + $signatureVersion + ); + return SignatureProvider::resolve($provider, $signatureVersion, $name, $region); }; $this->handlerList->appendSign( @@ -607,9 +604,19 @@ private function addEndpointV2Middleware() ); } + /** + * Appends the user agent middleware. + * This middleware MUST be appended after the + * signature middleware `addSignatureMiddleware`, + * so that metrics around signatures are properly + * captured. + * + * @param $args + * @return void + */ private function addUserAgentMiddleware($args) { - $this->getHandlerList()->prependSign( + $this->getHandlerList()->appendSign( UserAgentMiddleware::wrap($args), 'user-agent' ); diff --git a/src/Command.php b/src/Command.php index 3304fb5f05..f6c3991355 100644 --- a/src/Command.php +++ b/src/Command.php @@ -29,7 +29,12 @@ class Command implements CommandInterface * @param array $args Arguments to pass to the command * @param HandlerList $list Handler list */ - public function __construct($name, array $args = [], ?HandlerList $list = null) + public function __construct( + $name, + array $args = [], + ?HandlerList $list = null, + ?MetricsBuilder $metricsBuilder = null + ) { $this->name = $name; $this->data = $args; @@ -41,7 +46,7 @@ public function __construct($name, array $args = [], ?HandlerList $list = null) if (!isset($this->data['@context'])) { $this->data['@context'] = []; } - $this->metricsBuilder = new MetricsBuilder(); + $this->metricsBuilder = $metricsBuilder ?: new MetricsBuilder(); } public function __clone() diff --git a/src/Credentials/AssumeRoleCredentialProvider.php b/src/Credentials/AssumeRoleCredentialProvider.php index 416d79514e..c4c7635907 100644 --- a/src/Credentials/AssumeRoleCredentialProvider.php +++ b/src/Credentials/AssumeRoleCredentialProvider.php @@ -52,7 +52,10 @@ public function __invoke() $client = $this->client; return $client->assumeRoleAsync($this->assumeRoleParams) ->then(function (Result $result) { - return $this->client->createCredentials($result); + return $this->client->createCredentials( + $result, + CredentialSources::STS_ASSUME_ROLE + ); })->otherwise(function (\RuntimeException $exception) { throw new CredentialsException( "Error in retrieving assume role credentials.", diff --git a/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php b/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php index 7e8057e9dd..ea70522365 100644 --- a/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php +++ b/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php @@ -36,6 +36,8 @@ class AssumeRoleWithWebIdentityCredentialProvider /** @var integer */ private $tokenFileReadAttempts; + /** @var string */ + private $source; /** * The constructor attempts to load config from environment variables. @@ -43,6 +45,8 @@ class AssumeRoleWithWebIdentityCredentialProvider * - WebIdentityTokenFile: full path of token filename * - RoleArn: arn of role to be assumed * - SessionName: (optional) set by SDK if not provided + * - source: To identify if the provider was sourced by a profile or + * from environment definition. Default will be `sts_web_id_token`. * * @param array $config Configuration options * @throws \InvalidArgumentException @@ -66,15 +70,9 @@ public function __construct(array $config = []) $this->retries = (int) getenv(self::ENV_RETRIES) ?: (isset($config['retries']) ? $config['retries'] : 3); $this->authenticationAttempts = 0; $this->tokenFileReadAttempts = 0; - - $this->session = isset($config['SessionName']) - ? $config['SessionName'] - : 'aws-sdk-php-' . round(microtime(true) * 1000); - - $region = isset($config['region']) - ? $config['region'] - : 'us-east-1'; - + $this->session = $config['SessionName'] + ?? 'aws-sdk-php-' . round(microtime(true) * 1000); + $region = $config['region'] ?? 'us-east-1'; if (isset($config['client'])) { $this->client = $config['client']; } else { @@ -84,6 +82,9 @@ public function __construct(array $config = []) 'version' => 'latest' ]); } + + $this->source = $config['source'] + ?? CredentialSources::STS_WEB_ID_TOKEN; } /** @@ -160,7 +161,10 @@ public function __invoke() $this->authenticationAttempts++; } - yield $this->client->createCredentials($result); + yield $this->client->createCredentials( + $result, + $this->source + ); }); } } diff --git a/src/Credentials/CredentialProvider.php b/src/Credentials/CredentialProvider.php index 57238f0562..65c0dccf10 100644 --- a/src/Credentials/CredentialProvider.php +++ b/src/Credentials/CredentialProvider.php @@ -302,7 +302,8 @@ public static function env() $secret, $token, null, - $accountId + $accountId, + CredentialSources::ENVIRONMENT ) ); } @@ -417,7 +418,8 @@ public static function assumeRoleWithWebIdentityCredentialProvider(array $config 'WebIdentityTokenFile' => $tokenFromEnv, 'SessionName' => $sessionName, 'client' => $stsClient, - 'region' => $region + 'region' => $region, + 'source' => CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN ]); return $provider(); @@ -446,7 +448,8 @@ public static function assumeRoleWithWebIdentityCredentialProvider(array $config 'WebIdentityTokenFile' => $profile['web_identity_token_file'], 'SessionName' => $sessionName, 'client' => $stsClient, - 'region' => $region + 'region' => $region, + 'source' => CredentialSources::PROFILE_STS_WEB_ID_TOKEN ]); return $provider(); @@ -553,7 +556,8 @@ public static function ini($profile = null, $filename = null, array $config = [] $data[$profile]['aws_secret_access_key'], $data[$profile]['aws_session_token'], null, - !empty($data[$profile]['aws_account_id']) ? $data[$profile]['aws_account_id'] : null + $data[$profile]['aws_account_id'] ?? null, + CredentialSources::PROFILE ) ); }; @@ -641,7 +645,8 @@ public static function process($profile = null, $filename = null) $processData['SecretAccessKey'], $processData['SessionToken'], $expires, - $accountId + $accountId, + CredentialSources::PROCESS ) ); }; @@ -724,7 +729,10 @@ private static function loadRoleProfile( 'RoleArn' => $roleArn, 'RoleSessionName' => $roleSessionName ]); - $credentials = $stsClient->createCredentials($result); + $credentials = $stsClient->createCredentials( + $result, + CredentialSources::STS_ASSUME_ROLE + ); return Promise\Create::promiseFor($credentials); } @@ -918,7 +926,8 @@ private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], $expiration, - $ssoProfile['sso_account_id'] + $ssoProfile['sso_account_id'], + CredentialSources::SSO ) ); } @@ -978,7 +987,8 @@ private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $fil $ssoCredentials['secretAccessKey'], $ssoCredentials['sessionToken'], $expiration, - $ssoProfile['sso_account_id'] + $ssoProfile['sso_account_id'], + CredentialSources::SSO_LEGACY ) ); } diff --git a/src/Credentials/CredentialSources.php b/src/Credentials/CredentialSources.php new file mode 100644 index 0000000000..6480b7c838 --- /dev/null +++ b/src/Credentials/CredentialSources.php @@ -0,0 +1,22 @@ +key = trim((string) $key); $this->secret = trim((string) $secret); $this->token = $token; $this->expires = $expires; $this->accountId = $accountId; + $this->source = $source; } public static function __set_state(array $state) @@ -42,7 +51,8 @@ public static function __set_state(array $state) $state['secret'], $state['token'], $state['expires'], - $state['accountId'] + $state['accountId'], + $state['source'] ?? null ); } @@ -76,6 +86,11 @@ public function getAccountId() return $this->accountId; } + public function getSource() + { + return $this->source; + } + public function toArray() { return [ @@ -83,7 +98,8 @@ public function toArray() 'secret' => $this->secret, 'token' => $this->token, 'expires' => $this->expires, - 'accountId' => $this->accountId + 'accountId' => $this->accountId, + 'source' => $this->source ]; } @@ -111,6 +127,7 @@ public function __unserialize($data) $this->token = $data['token']; $this->expires = $data['expires']; $this->accountId = $data['accountId'] ?? null; + $this->source = $data['source'] ?? null; } /** diff --git a/src/Credentials/EcsCredentialProvider.php b/src/Credentials/EcsCredentialProvider.php index 893ee09b25..0d8c11928d 100644 --- a/src/Credentials/EcsCredentialProvider.php +++ b/src/Credentials/EcsCredentialProvider.php @@ -91,7 +91,8 @@ public function __invoke() $result['SecretAccessKey'], $result['Token'], strtotime($result['Expiration']), - $result['AccountId'] ?? null + $result['AccountId'] ?? null, + CredentialSources::ECS ); })->otherwise(function ($reason) { $reason = is_array($reason) ? $reason['exception'] : $reason; diff --git a/src/Credentials/InstanceProfileProvider.php b/src/Credentials/InstanceProfileProvider.php index 7a7a178b6f..c17a564133 100644 --- a/src/Credentials/InstanceProfileProvider.php +++ b/src/Credentials/InstanceProfileProvider.php @@ -227,7 +227,8 @@ public function __invoke($previousCredentials = null) $result['SecretAccessKey'], $result['Token'], strtotime($result['Expiration']), - $result['AccountId'] ?? null + $result['AccountId'] ?? null, + CredentialSources::IMDS ); } diff --git a/src/EndpointV2/EndpointV2Middleware.php b/src/EndpointV2/EndpointV2Middleware.php index b9c970d238..0f141037c6 100644 --- a/src/EndpointV2/EndpointV2Middleware.php +++ b/src/EndpointV2/EndpointV2Middleware.php @@ -99,14 +99,13 @@ public function __invoke(CommandInterface $command) $operation = $this->api->getOperation($command->getName()); $commandArgs = $command->toArray(); $providerArgs = $this->resolveArgs($commandArgs, $operation); - $this->hookAccountIdMetric( - $providerArgs[self::ACCOUNT_ID_PARAM] ?? null, - $command - ); + if (!empty($providerArgs[self::ACCOUNT_ID_PARAM])) { + $command->getMetricsBuilder()->append(MetricsBuilder::RESOLVED_ACCOUNT_ID); + } $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs); - $this->hookAccountIdEndpointMetric( - $endpoint, - $command + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'account_id_endpoint', + $endpoint->getUrl() ); if (!empty($authSchemes = $endpoint->getProperty('authSchemes'))) { $this->applyAuthScheme( @@ -402,23 +401,4 @@ private function resolveAccountId(): ?string return $identity->getAccountId(); } - - private function hookAccountIdMetric($accountId, &$command) - { - if (!empty($accountId)) { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::RESOLVED_ACCOUNT_ID - ); - } - } - - private function hookAccountIdEndpointMetric($endpoint, $command) - { - $regex = "/^(https?:\/\/\d{12}\.[^\s\/$.?#].\S*)$/"; - if (preg_match($regex, $endpoint->getUrl())) { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::ACCOUNT_ID_ENDPOINT - ); - } - } } diff --git a/src/MetricsBuilder.php b/src/MetricsBuilder.php index 4bb803fb84..a267088702 100644 --- a/src/MetricsBuilder.php +++ b/src/MetricsBuilder.php @@ -2,13 +2,16 @@ namespace Aws; +use Aws\Credentials\CredentialsInterface; +use Aws\Credentials\CredentialSources; + /** + * A placeholder for gathering metrics in a request. + * * @internal */ final class MetricsBuilder { - const COMMAND_METRICS_BUILDER = "CommandMetricsBuilder"; - const RESOURCE_MODEL = "A"; const WAITER = "B"; const PAGINATOR = "C"; const RETRY_MODE_LEGACY = "D"; @@ -17,6 +20,8 @@ final class MetricsBuilder const S3_TRANSFER = "G"; const S3_CRYPTO_V1N = "H"; const S3_CRYPTO_V2 = "I"; + const S3_EXPRESS_BUCKET = "J"; + const GZIP_REQUEST_COMPRESSION = "L"; const ENDPOINT_OVERRIDE = "N"; const ACCOUNT_ID_ENDPOINT = "O"; const ACCOUNT_ID_MODE_PREFERRED = "P"; @@ -29,6 +34,18 @@ final class MetricsBuilder const FLEXIBLE_CHECKSUMS_REQ_CRC64 = "W"; const FLEXIBLE_CHECKSUMS_REQ_SHA1 = "X"; const FLEXIBLE_CHECKSUMS_REQ_SHA256 = "Y"; + const CREDENTIALS_CODE = "e"; + const CREDENTIALS_ENV_VARS = "g"; + const CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN = "h"; + const CREDENTIALS_STS_ASSUME_ROLE = "i"; + const CREDENTIALS_STS_ASSUME_ROLE_WEB_ID = "k"; + const CREDENTIALS_PROFILE = "n"; + const CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN = "q"; + const CREDENTIALS_HTTP = "z"; + const CREDENTIALS_IMDS = "0"; + const CREDENTIALS_PROCESS = "w"; + const CREDENTIALS_SSO = "s"; + const CREDENTIALS_SSO_LEGACY = "u"; /** @var int */ private static $MAX_METRICS_SIZE = 1024; // 1KB or 1024 B @@ -74,15 +91,12 @@ private function encode(): string } /** - * Appends a metric into the internal metrics holder. - * It checks if the metric can be appended before doing so. - * If the metric can be appended then, it is added into the - * metrics holder and the current metrics size is increased - * by summing the length of the metric being appended plus the length - * of the separator used for encoding. + * Appends a metric to the internal metrics holder after validating it. + * Increases the current metrics size by the length of the new metric + * plus the length of the encoding separator. * Example: $currentSize = $currentSize + len($newMetric) + len($separator) * - * @param string $metric + * @param string $metric The metric to append. * * @return void */ @@ -97,18 +111,161 @@ public function append(string $metric): void } /** - * Validates if a metric can be appended by verifying if the current - * metrics size plus the new metric plus the length of the separator - * exceeds the metrics size limit. It also checks if the metric already - * exists, if so then it returns false. - * Example: metric can be appended just if: - * $currentSize + len($newMetric) + len($metricSeparator) <= MAX_SIZE + * Receives a feature group and a value to identify which one is the metric. + * For example, a group could be `signature` and a value could be `v4a`, + * then the metric will be `SIGV4A_SIGNING`. + * + * @param string $featureGroup the feature group such as `signature`. + * @param mixed $value the value for identifying the metric. + * + * @return void + */ + public function identifyMetricByValueAndAppend( + string $featureGroup, + $value + ): void + { + if (empty($value)) { + return; + } + + static $appendMetricFns = [ + 'signature' => 'appendSignatureMetric', + 'request_compression' => 'appendRequestCompressionMetric', + 'account_id_endpoint' => 'appendAccountIdEndpoint', + 'request_checksum' => 'appendRequestChecksumMetric', + 'credentials' => 'appendCredentialsMetric' + ]; + + $fn = $appendMetricFns[$featureGroup]; + $this->{$fn}($value); + } + + /** + * Appends the signature metric based on the signature value. + * + * @param string $signature + * + * @return void + */ + private function appendSignatureMetric(string $signature): void + { + if ($signature === 'v4-s3express') { + $this->append(MetricsBuilder::S3_EXPRESS_BUCKET); + } elseif ($signature === 'v4a') { + $this->append(MetricsBuilder::SIGV4A_SIGNING); + } + } + + /** + * Appends the request compression metric based on the format resolved. + * + * @param string $format + * + * @return void + */ + private function appendRequestCompressionMetric(string $format): void + { + if ($format === 'gzip') { + $this->append(MetricsBuilder::GZIP_REQUEST_COMPRESSION); + } + } + + /** + * Appends the account id endpoint metric by validating if the + * endpoint contains an account id in its URL. + * + * @param string $endpoint + * + * @return void + */ + private function appendAccountIdEndpoint(string $endpoint): void + { + $regex = "/^(https?:\/\/\d{12}\.[^\s\/$.?#].\S*)$/"; + if (preg_match($regex, $endpoint)) { + $this->append(MetricsBuilder::ACCOUNT_ID_ENDPOINT); + } + } + + /** + * Appends the request checksum metric based on the algorithm. + * + * @param string $algorithm + * + * @return void + */ + private function appendRequestChecksumMetric(string $algorithm): void + { + if ($algorithm === 'crc32') { + $this->append(MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32); + } elseif ($algorithm === 'crc32c') { + $this->append(MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32C); + } elseif ($algorithm === 'crc64') { + $this->append(MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC64); + } elseif ($algorithm === 'sha1') { + $this->append(MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA1); + } elseif ($algorithm === 'sha256') { + $this->append(MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA256); + } + } + + + /** + * Appends the credentials metric based on the type of credentials + * resolved. + * + * @param CredentialsInterface $credentials + * + * @return void + */ + private function appendCredentialsMetric( + CredentialsInterface $credentials + ): void + { + $source = $credentials->toArray()['source'] ?? null; + if (empty($source)) { + return; + } + + if ($source === CredentialSources::STATIC) { + $this->append(MetricsBuilder::CREDENTIALS_CODE); + } elseif ($source === CredentialSources::ENVIRONMENT) { + $this->append(MetricsBuilder::CREDENTIALS_ENV_VARS); + } elseif ($source === CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN) { + $this->append(MetricsBuilder::CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN); + } elseif ($source === CredentialSources::STS_ASSUME_ROLE) { + $this->append(MetricsBuilder::CREDENTIALS_STS_ASSUME_ROLE); + } elseif ($source === CredentialSources::STS_WEB_ID_TOKEN) { + $this->append(MetricsBuilder::CREDENTIALS_STS_ASSUME_ROLE_WEB_ID); + } elseif ($source === CredentialSources::PROFILE) { + $this->append(MetricsBuilder::CREDENTIALS_PROFILE); + } elseif ($source === CredentialSources::IMDS) { + $this->append(MetricsBuilder::CREDENTIALS_IMDS); + } elseif ($source === CredentialSources::ECS) { + $this->append(MetricsBuilder::CREDENTIALS_HTTP); + } elseif ($source === CredentialSources::PROFILE_STS_WEB_ID_TOKEN) { + $this->append(MetricsBuilder::CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN); + } elseif ($source === CredentialSources::PROCESS) { + $this->append(MetricsBuilder::CREDENTIALS_PROCESS); + } elseif ($source === CredentialSources::SSO) { + $this->append(MetricsBuilder::CREDENTIALS_SSO); + } elseif ($source === CredentialSources::SSO_LEGACY) { + $this->append(MetricsBuilder::CREDENTIALS_SSO_LEGACY); + } + } + + /** + * Validates if a metric can be appended by ensuring the total size, + * including the new metric and separator, does not exceed the limit. + * Also checks that the metric does not already exist. + * Example: Appendable if: + * $currentSize + len($newMetric) + len($separator) <= MAX_SIZE * and: - * $newMetric not in $existentMetrics + * $newMetric not in $existingMetrics * - * @param string $newMetric + * @param string $newMetric The metric to validate. * - * @return bool + * @return bool True if the metric can be appended, false otherwise. */ private function canMetricBeAppended(string $newMetric): bool { @@ -149,6 +306,16 @@ public static function fromCommand(CommandInterface $command): MetricsBuilder return $command->getMetricsBuilder(); } + /** + * Helper method for appending a metrics capture middleware into a + * handler stack given. The middleware appended here is on top of the + * build step. + * + * @param HandlerList $handlerList + * @param $metric + * + * @return void + */ public static function appendMetricsCaptureMiddleware( HandlerList $handlerList, $metric diff --git a/src/Middleware.php b/src/Middleware.php index 6a8c37a1a0..8ce1997597 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -151,6 +151,12 @@ function (TokenInterface $token) return $credentialPromise->then( function (CredentialsInterface $creds) use ($handler, $command, $signer, $request) { + // Capture credentials metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'credentials', + $creds + ); + return $handler( $command, $signer->signRequest($request, $creds) diff --git a/src/RequestCompressionMiddleware.php b/src/RequestCompressionMiddleware.php index a83e593fdc..667761df46 100644 --- a/src/RequestCompressionMiddleware.php +++ b/src/RequestCompressionMiddleware.php @@ -67,6 +67,12 @@ public function __invoke(CommandInterface $command, RequestInterface $request) $this->encodings = $compressionInfo['encodings']; $request = $this->compressRequestBody($request); + // Capture request compression metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'request_compression', + $request->getHeaderLine('content-encoding') + ); + return $nextHandler($command, $request); } @@ -161,4 +167,4 @@ private function isValidCompressionSize($compressionSize) . 'non-negative integer value between 0 and 10485760 bytes, inclusive.' ); } -} \ No newline at end of file +} diff --git a/src/S3/ApplyChecksumMiddleware.php b/src/S3/ApplyChecksumMiddleware.php index ba79d8b1aa..085d288a7c 100644 --- a/src/S3/ApplyChecksumMiddleware.php +++ b/src/S3/ApplyChecksumMiddleware.php @@ -84,7 +84,10 @@ public function __invoke( ); } - $this->hookChecksumAlgorithmMetric($requestedAlgorithm, $command); + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'request_checksum', + $requestedAlgorithm + ); return $next($command, $request); } @@ -98,7 +101,9 @@ public function __invoke( //S3Express doesn't support MD5; default to crc32 instead if ($this->isS3Express($command)) { $request = $this->addAlgorithmHeader('crc32', $request, $body); - $this->hookChecksumAlgorithmMetric('crc32', $command); + $command->getMetricsBuilder()->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32 + ); } elseif (!$request->hasHeader('Content-MD5')) { // Set the content MD5 header for operations that require it. $request = $request->withHeader( @@ -116,7 +121,9 @@ public function __invoke( 'X-Amz-Content-Sha256', $command['ContentSHA256'] ); - $this->hookChecksumAlgorithmMetric('sha256', $command); + $command->getMetricsBuilder()->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA256 + ); } return $next($command, $request); @@ -150,34 +157,4 @@ private function isS3Express(CommandInterface $command): bool return isset($command['@context']['signing_service']) && $command['@context']['signing_service'] === 's3express'; } - - private function hookChecksumAlgorithmMetric($algorithm, $command) - { - if (empty($algorithm)) { - return; - } - - if ($algorithm === 'crc32') { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32 - ); - } elseif ($algorithm === 'crc32c') { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32C - ); - } elseif ($algorithm === 'crc64') { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC64 - ); - } elseif ($algorithm === 'sha1') { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA1 - ); - } elseif ($algorithm === 'sha256') { - MetricsBuilder::fromCommand($command)->append( - MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA256 - ); - } - - } } diff --git a/src/Sts/StsClient.php b/src/Sts/StsClient.php index 568e643edf..4d0f25f91a 100644 --- a/src/Sts/StsClient.php +++ b/src/Sts/StsClient.php @@ -70,7 +70,7 @@ public function __construct(array $args) * @return Credentials * @throws \InvalidArgumentException if the result contains no credentials */ - public function createCredentials(Result $result) + public function createCredentials(Result $result, $source=null) { if (!$result->hasKey('Credentials')) { throw new \InvalidArgumentException('Result contains no credentials'); @@ -95,7 +95,8 @@ public function createCredentials(Result $result) $credentials['SecretAccessKey'], isset($credentials['SessionToken']) ? $credentials['SessionToken'] : null, $expiration, - $accountId + $accountId, + $source ); } diff --git a/src/UserAgentMiddleware.php b/src/UserAgentMiddleware.php index d18eab309e..1ec0da2dd2 100644 --- a/src/UserAgentMiddleware.php +++ b/src/UserAgentMiddleware.php @@ -6,11 +6,33 @@ use Closure; use Psr\Http\Message\RequestInterface; +/** + * Builds and injects the user agent header values. + * This middleware must be appended into step where all the + * metrics to be gathered are already resolved. As of now it should be + * after the signing step. + */ class UserAgentMiddleware { const AGENT_VERSION = 2.1; - /** @var callable */ + static $userAgentFnList = [ + 'sdkVersion', + 'userAgentVersion', + 'hhvmVersion', + 'osName', + 'langVersion', + 'execEnv', + 'endpointDiscovery', + 'appId', + 'metrics' + ]; + static $metricsFnList = [ + 'endpointMetric', + 'accountIdModeMetric', + 'retryConfigMetric', + ]; + /** @var callable */ private $nextHandler; /** @var array */ private $args; @@ -100,19 +122,8 @@ private function requestWithUserAgentHeader(RequestInterface $request): RequestI */ private function buildUserAgentValue(): array { - static $fnList = [ - 'sdkVersion', - 'userAgentVersion', - 'hhvmVersion', - 'osName', - 'langVersion', - 'execEnv', - 'endpointDiscovery', - 'appId', - 'metrics' - ]; $userAgentValue = []; - foreach ($fnList as $fn) { + foreach (self::$userAgentFnList as $fn) { $val = $this->{$fn}(); if (!empty($val)) { $userAgentValue[] = $val; @@ -201,6 +212,12 @@ private function execEnv(): string return ""; } + /** + * Returns the user agent value for endpoint discovery as cfg. + * This feature is deprecated. + * + * @return string + */ private function endpointDiscovery(): string { $args = $this->args; @@ -242,12 +259,7 @@ private function appId(): string */ private function metrics(): string { - static $metricsFn = [ - 'endpointMetric', - 'accountIdModeMetric', - 'retryConfigMetric' - ]; - foreach ($metricsFn as $fn) { + foreach (self::$metricsFnList as $fn) { $this->{$fn}(); } diff --git a/tests/AwsClientTest.php b/tests/AwsClientTest.php index 20998b691e..e3e6cc006c 100644 --- a/tests/AwsClientTest.php +++ b/tests/AwsClientTest.php @@ -986,25 +986,6 @@ public function testClientParameterOverridesDefaultAccountIdEndpointModeBuiltIns self::assertEquals($expectedAccountIdEndpointMode, $builtIns['AWS::Auth::AccountIdEndpointMode']); } - public function testAppendsC2jMetricsCaptureMiddleware() - { - $client = new S3Client([ - 'region' => 'us-east-2', - 'http_handler' => function (RequestInterface $request) { - $this->assertTrue( - in_array( - MetricsBuilder::RESOURCE_MODEL, - $this->getMetricsAsArray($request) - ) - ); - - return new Response(); - } - ]); - - $client->listBuckets(); - } - public function testAppendsUserAgentMiddleware() { $client = new S3Client([ diff --git a/tests/Credentials/CredentialProviderTest.php b/tests/Credentials/CredentialProviderTest.php index ba8afebda6..73701f1b53 100644 --- a/tests/Credentials/CredentialProviderTest.php +++ b/tests/Credentials/CredentialProviderTest.php @@ -4,6 +4,7 @@ use Aws\Api\DateTimeResult; use Aws\Credentials\CredentialProvider; use Aws\Credentials\Credentials; +use Aws\Credentials\CredentialSources; use Aws\Credentials\EcsCredentialProvider; use Aws\Credentials\InstanceProfileProvider; use Aws\History; @@ -210,10 +211,31 @@ public function testCreatesFromIniFile($iniFile, Credentials $expectedCreds) public function iniFileProvider() { - $credentials = new Credentials('foo', 'bar', 'baz'); + $credentials = new Credentials( + 'foo', + 'bar', + 'baz', + null, + null, + CredentialSources::PROFILE + ); $testAccountId = 'foo'; - $credentialsWithAccountId = new Credentials('foo', 'bar', 'baz', null, $testAccountId); - $credentialsWithEquals = new Credentials('foo', 'bar', 'baz='); + $credentialsWithAccountId = new Credentials( + 'foo', + 'bar', + 'baz', + null, + $testAccountId, + CredentialSources::PROFILE + ); + $credentialsWithEquals = new Credentials( + 'foo', + 'bar', + 'baz=', + null, + null, + CredentialSources::PROFILE + ); $standardIni = << "bar", "token" => "baz", "expires" => null, - "accountId" => null + "accountId" => null, + 'source' => CredentialSources::PROFILE ]; putenv('HOME=' . dirname($dir)); $creds = call_user_func( diff --git a/tests/Credentials/CredentialsTest.php b/tests/Credentials/CredentialsTest.php index b5aef32358..eea715e5d8 100644 --- a/tests/Credentials/CredentialsTest.php +++ b/tests/Credentials/CredentialsTest.php @@ -2,6 +2,7 @@ namespace Aws\Test\Credentials; use Aws\Credentials\Credentials; +use Aws\Credentials\CredentialSources; use Aws\Identity\AwsCredentialIdentity; use Aws\Identity\AwsCredentialIdentityInterface; use Aws\Identity\IdentityInterface; @@ -27,7 +28,8 @@ public function testHasGetters() 'secret' => 'baz', 'token' => 'tok', 'expires' => $exp, - 'accountId' => $accountId + 'accountId' => $accountId, + 'source' => CredentialSources::STATIC ], $creds->toArray()); } @@ -51,7 +53,8 @@ public function testSerialization() 'secret' => 'secret-value', 'token' => null, 'expires' => null, - 'accountId' => null + 'accountId' => null, + 'source' => CredentialSources::STATIC ], $actual); $accountId = 'foo'; $credentials = new Credentials('key-value', 'secret-value', 'token-value', 10, $accountId); @@ -62,7 +65,8 @@ public function testSerialization() 'secret' => 'secret-value', 'token' => 'token-value', 'expires' => 10, - 'accountId' => $accountId + 'accountId' => $accountId, + 'source' => CredentialSources::STATIC ], $actual); } diff --git a/tests/MetricsBuilderTestTrait.php b/tests/MetricsBuilderTestTrait.php index 82f322d2e8..ccce8754af 100644 --- a/tests/MetricsBuilderTestTrait.php +++ b/tests/MetricsBuilderTestTrait.php @@ -7,7 +7,7 @@ trait MetricsBuilderTestTrait { public function getMetricsAsArray(RequestInterface $request): array { - $regex = "/([mM]\/)([A-Za-z,]+)/"; + $regex = "/([mM]\/)([A-Za-z,0-9]+)/"; if (preg_match( $regex, $request->getHeaderLine('User-Agent'), diff --git a/tests/Sts/StsClientTest.php b/tests/Sts/StsClientTest.php index 6e7fc00a76..990ff8801a 100644 --- a/tests/Sts/StsClientTest.php +++ b/tests/Sts/StsClientTest.php @@ -5,6 +5,7 @@ use Aws\Credentials\CredentialProvider; use Aws\Credentials\Credentials; use Aws\Credentials\CredentialsInterface; +use Aws\Credentials\CredentialSources; use Aws\Endpoint\PartitionEndpointProvider; use Aws\Exception\CredentialsException; use Aws\LruArrayCache; @@ -199,7 +200,8 @@ public function stsAssumeRoleOperationsDataProvider(): array "accountId" => "foobar", "accessKeyId" => "foo", "secretAccessKey" => "bar", - "sessionToken" => "baz" + "sessionToken" => "baz", + "source" => CredentialSources::STS_ASSUME_ROLE ] ] ]; @@ -308,7 +310,8 @@ public function stsAssumeRoleWithWebIdentityOperationsDataProvider(): array "accountId" => "foobar", "accessKeyId" => "foo", "secretAccessKey" => "bar", - "sessionToken" => "baz" + "sessionToken" => "baz", + "source" => CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN ] ] ]; @@ -413,7 +416,8 @@ private function normalizeExpectedResponse(array $expectedResponse): Credentials $expectedResponse['secretAccessKey'] ?? null, $expectedResponse['sessionToken'] ?? null, $expectedResponse['expires'] ?? null, - $expectedResponse['accountId'] ?? null + $expectedResponse['accountId'] ?? null, + $expectedResponse['source'] ?? null ); } @@ -457,4 +461,26 @@ private function createTestWebIdentityToken(): string return $tokenPath; } + + public function testCreateCredentialsAddSource() + { + $result = new Result([ + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'foo' + ] + ]); + $stsClient = new StsClient([ + 'region' => 'us-east-1' + ]); + $credentials = $stsClient->createCredentials( + $result, + CredentialSources::PROFILE + ); + $this->assertNotEmpty($credentials->getSource()); + $this->assertEquals( + CredentialSources::PROFILE, + $credentials->getSource() + ); + } } diff --git a/tests/UserAgentMiddlewareTest.php b/tests/UserAgentMiddlewareTest.php index c5e64ac9fd..eb0e869b49 100644 --- a/tests/UserAgentMiddlewareTest.php +++ b/tests/UserAgentMiddlewareTest.php @@ -1,24 +1,101 @@ deferFns) > 0) { - $fn = array_pop($this->deferFns); - $fn(); + $this->envValues = [ + 'AWS_EXECUTION_ENV' => getenv('AWS_EXECUTION_ENV'), + 'AWS_ACCESS_KEY_ID' => getenv('AWS_ACCESS_KEY_ID'), + 'AWS_SECRET_ACCESS_KEY' => getenv('AWS_SECRET_ACCESS_KEY'), + 'HOME' => getenv('HOME'), + CredentialProvider::ENV_ARN => getenv( + CredentialProvider::ENV_ARN + ), + CredentialProvider::ENV_TOKEN_FILE => getenv( + CredentialProvider::ENV_TOKEN_FILE + ), + CredentialProvider::ENV_ROLE_SESSION_NAME => getenv( + CredentialProvider::ENV_ROLE_SESSION_NAME + ), + CredentialProvider::ENV_PROFILE => getenv( + CredentialProvider::ENV_PROFILE + ), + ]; + // Create temp dirs + $tempDir = sys_get_temp_dir() . '/test-user-agent'; + $awsDir = $tempDir . "/.aws"; + if (!is_dir($tempDir)) { + mkdir($tempDir, 0777, true); + mkdir($awsDir, 0777, true); } + + $this->tempDir = $tempDir; + $this->awsDir = $awsDir; + // Clean up env + putenv(CredentialProvider::ENV_ARN); + putenv(CredentialProvider::ENV_TOKEN_FILE); + putenv(CredentialProvider::ENV_ROLE_SESSION_NAME); + putenv(CredentialProvider::ENV_PROFILE); + putenv('AWS_ACCESS_KEY_ID'); + putenv('AWS_SECRET_ACCESS_KEY'); + } + + protected function tearDown(): void + { + foreach ($this->envValues as $key => $envValue) { + if ($envValue === false) { + putenv("$key"); + } else { + putenv("$key=$envValue"); + } + } + + $this->cleanUpDir($this->tempDir); } /** @@ -63,6 +140,7 @@ public function testUserAgentContainsValue(array $args, string $expected) } $userAgent = $request->getHeaderLine('User-Agent'); $userAgentValues = explode(' ', $userAgent); + $this->assertTrue(in_array($expected, $userAgentValues)); }); $request = new Request('post', 'foo', [], 'buzz'); @@ -74,9 +152,9 @@ public function testUserAgentContainsValue(array $args, string $expected) * per iteration. * Example: yield [$arguments, 'ExpectedValue'] * - * @return Generator + * @return \Generator */ - public function userAgentCasesDataProvider(): Generator + public function userAgentCasesDataProvider(): \Generator { $userAgentCases = [ 'sdkVersion' => [[], 'aws-sdk-php/' . Sdk::VERSION], @@ -113,17 +191,8 @@ public function userAgentCasesDataProvider(): Generator 'langVersion' => [[], 'lang/php#' . phpversion()], 'execEnv' => function (): array { $expectedEnv = "LambdaFooEnvironment"; - $currentEnv = getenv('AWS_EXECUTION_ENV'); putenv("AWS_EXECUTION_ENV={$expectedEnv}"); - $this->deferFns[] = function () use ($currentEnv) { - if ($currentEnv !== false) { - putenv("AWS_EXECUTION_ENV={$currentEnv}"); - } else { - putenv('AWS_EXECUTION_ENV'); - } - }; - return [[], $expectedEnv]; }, 'appId' => function (): array { @@ -274,4 +343,1135 @@ public function testUserAgentValueStartsWithSdkVersionString() $request = new Request('post', 'foo', [], 'buzz'); $middleware(new Command('buzz'), $request); } + + /** + * Tests user agent captures the waiter metric. + * + * @return void + */ + public function testUserAgentCaptureWaiterMetric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue(in_array(MetricsBuilder::WAITER, $metrics)); + + return new Response(); + } + ]); + $waiter = $s3Client->getWaiter('BucketExists', ['Bucket' => 'foo-bucket']); + $waiter->promise()->wait(); + } + + /** + * Tests user agent captures the paginator metric. + * + * @return void + */ + public function testUserAgentCapturePaginatorMetric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::PAGINATOR, $metrics) + ); + + return new Response(); + } + ]); + $paginator = $s3Client->getPaginator('ListObjects', ['Bucket' => 'foo-bucket']); + $paginator->current(); + } + + /** + * Tests user agent captures retry config metric. + * + * @dataProvider retryConfigMetricProvider + * + * @return void + */ + public function testUserAgentCaptureRetryConfigMetric( + $retryMode, + $expectedMetric + ) + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'retries' => [ + 'mode' => $retryMode + ], + 'http_handler' => function ( + RequestInterface $request + ) use($expectedMetric) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array($expectedMetric, $metrics) + ); + + return new Response(); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Retry config metrics provider. + * + * @return array[] + */ + public function retryConfigMetricProvider(): array + { + return [ + 'retryAdaptive' => [ + 'mode' => 'adaptive', + 'metric' => MetricsBuilder::RETRY_MODE_ADAPTIVE + ], + 'retryStandard' => [ + 'mode' => 'standard', + 'metric' => MetricsBuilder::RETRY_MODE_STANDARD + ], + 'retryLegacy' => [ + 'mode' => 'legacy', + 'metric' => MetricsBuilder::RETRY_MODE_LEGACY + ], + ]; + } + + /** + * Tests user agent captures the s3 transfer metric. + * + * @return void + */ + public function testUserAgentCaptureS3TransferMetric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::S3_TRANSFER, $metrics) + ); + + return new Response(); + } + ]); + $transfer = new Transfer($s3Client, 's3://foo', './buzz'); + $transfer->promise()->wait(); + } + + /** + * Tests user agent captures the s3 encryption client v1 metric. + * + * @return void + */ + public function testUserAgentCaptureS3CryptoV1Metric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'handler' => function ( + CommandInterface $_, + RequestInterface $request + ) { + + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::S3_CRYPTO_V1N, $metrics) + ); + + return new Result([ + 'Body' => 'This is a test body' + ]); + } + ]); + $encryptionClient = $this->getMockBuilder(S3EncryptionClient::class) + ->setConstructorArgs([$s3Client]) + ->setMethods(['decrypt']) + ->getMock(); + $encryptionClient->expects($this->once()) + ->method('decrypt') + ->withAnyParameters() + ->willReturn(base64_encode('Test body')); + $materialProvider = $this->createMock(MaterialsProvider::class); + $materialProvider->expects($this->once()) + ->method('fromDecryptionEnvelope') + ->withAnyParameters() + ->willReturn($materialProvider); + $encryptionClient->getObject([ + 'Bucket' => 'foo', + 'Key' => 'foo', + '@MaterialsProvider' => $materialProvider + ]); + } + + /** + * Tests user agent captures the s3 crypto v2 metric. + * + * @return void + */ + public function testUserAgentCaptureS3CryptoV2Metric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'handler' => function ( + CommandInterface $_, + RequestInterface $request + ) { + + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::S3_CRYPTO_V2, $metrics) + ); + + return new Result([ + 'Body' => 'This is a test body' + ]); + } + ]); + $encryptionClient = $this->getMockBuilder(S3EncryptionClientV2::class) + ->setConstructorArgs([$s3Client]) + ->setMethods(['decrypt']) + ->getMock(); + $encryptionClient->expects($this->once()) + ->method('decrypt') + ->withAnyParameters() + ->willReturn(base64_encode('Test body')); + $materialProvider = $this->createMock(MaterialsProviderV2::class); + $encryptionClient->getObject([ + 'Bucket' => 'foo', + 'Key' => 'foo', + '@MaterialsProvider' => $materialProvider, + '@SecurityProfile' => 'V2' + ]); + } + + /** + * Tests user agent captures the s3 express signature metric. + * + * @return void + */ + public function testUserAgentCaptureS3ExpressBucketMetric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'signature_version' => 'v4-s3express', + 's3_express_identity_provider' => function ($_) { + return Create::promiseFor( + new Credentials( + 'foo', + 'foo', + 'foo', + null, + null + ) + ); + }, + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::S3_EXPRESS_BUCKET, $metrics) + ); + + return new Response(); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Tests user agent captures the s3 v4a signature metric. + * + * @return void + */ + public function testUserAgentCaptureSignatureV4AMetric() + { + if (!extension_loaded('awscrt')) { + $this->markTestSkipped('awscrt extension is not loaded!'); + } + + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'signature_version' => 'v4a', + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::SIGV4A_SIGNING, $metrics) + ); + + return new Response(); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Tests user agent captures the gzip request compression format. + * + * @return void + */ + public function testUserAgentCaptureGzipRequestCompressionMetric() + { + $cloudWatchClient = new CloudWatchClient([ + 'region' => 'us-east-2', + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::GZIP_REQUEST_COMPRESSION, $metrics) + ); + + return new Response( + 200, + [], + '' + ); + } + ]); + $cloudWatchClient->putMetricData([ + 'Namespace' => 'foo', + 'MetricData' => [], + '@request_min_compression_size_bytes' => 8 + ]); + } + + /** + * Tests user agent captures the account id endpoint metric. + * + * @return void + */ + public function testUserAgentCaptureAccountIdEndpointMetric() + { + $dynamoDbClient = new DynamoDbClient([ + 'region' => 'us-east-2', + 'credentials' => new Credentials( + 'foo', + 'foo', + 'foo', + null, + '123456789012' + ), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::ACCOUNT_ID_ENDPOINT, $metrics) + ); + + return new Response( + 200, + [], + '{}' + ); + } + ]); + $dynamoDbClient->listTables(); + } + + /** + * Tests user agent captures a resolved account id metric. + * + * @return void + */ + public function testUserAgentCaptureResolvedAccountIdMetric() + { + $dynamoDbClient = new DynamoDbClient([ + 'region' => 'us-east-2', + 'credentials' => new Credentials( + 'foo', + 'foo', + 'foo', + null, + '123456789012' + ), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::RESOLVED_ACCOUNT_ID, $metrics) + ); + + return new Response( + 200, + [], + '{}' + ); + } + ]); + $dynamoDbClient->listTables(); + } + + /** + * Tests user agent captures the flexible checksum metric. + * + * @param string $algorithm + * @param string $checksumMetric + * @param bool $supported + * + * @dataProvider flexibleChecksumTestProvider + * + * @return void + */ + public function testUserAgentCaptureFlexibleChecksumMetric( + string $algorithm, + string $checksumMetric, + bool $supported = true + ) + { + if (!$supported) { + $this->markTestSkipped( + "Algorithm {$algorithm} is not supported!" + ); + } + + $s3Client = new S3Client([ + 'region' => 'us-west-2', + 'api_provider' => ApiProvider::filesystem(__DIR__ . '/S3/fixtures'), + 'http_handler' => function (RequestInterface $request) + use ($checksumMetric) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array($checksumMetric, $metrics) + ); + + return new Response( + 200, + [], + '' + ); + } + ]); + $s3Client->putObject([ + 'Bucket' => 'foo', + 'Key' => 'foo', + 'Body' => 'Test body', + 'ChecksumAlgorithm' => $algorithm + ]); + } + + /** + * Data provider to test the different checksum metrics. + * + * @return array[] + */ + public function flexibleChecksumTestProvider(): array + { + return [ + 'metric_checksum_crc32' => [ + 'algorithm' => 'crc32', + 'expected_metric' => MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32 + ], + 'metric_checksum_crc32c' => [ + 'algorithm' => 'crc32c', + 'expected_metric' => MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32C, + 'supported' => extension_loaded('awscrt'), + ], + 'metric_checksum_crc64' => [ + 'algorithm' => 'crc64', + 'expected_metric' => MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC64, + 'supported' => false, + ], + 'metric_checksum_sha1' => [ + 'algorithm' => 'sha1', + 'expected_metric' => MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA1 + ], + 'metric_checksum_sha256' => [ + 'algorithm' => 'sha256', + 'expected_metric' => + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA256 + ], + ]; + } + + /** + * Test user agent captures metric from client instantiation credentials. + * + * @return void + */ + public function testUserAgentCaptureCredentialsCodeMetric() + { + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => [ + 'key' => 'foo', + 'secret' => 'foo' + ], + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_CODE, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric from environment credentials. + * + * @return void + */ + public function testUserAgentCaptureCredentialsEnvMetric() + { + putenv('AWS_ACCESS_KEY_ID=foo'); + putenv('AWS_SECRET_ACCESS_KEY=foo'); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_ENV_VARS, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric from web id token defined by env + * variables. + * + * @return void + * + * @throws \Exception + */ + public function testUserAgentCaptureCredentialsEnvStsWebIdTokenMetric() + { + $tokenPath = $this->awsDir . '/my-token.jwt'; + file_put_contents($tokenPath, 'token'); + $roleArn = 'arn:aws:iam::123456789012:role/role_name'; + // Set temporary env values + putenv(CredentialProvider::ENV_ARN . "={$roleArn}"); + putenv(CredentialProvider::ENV_TOKEN_FILE . "={$tokenPath}"); + putenv( + CredentialProvider::ENV_ROLE_SESSION_NAME . "=TestSession" + ); + // End setting env values + $result = [ + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'bar', + 'SessionToken' => 'baz', + 'Expiration' => DateTimeResult::fromEpoch(time() + 10) + ], + 'AssumedRoleUser' => [ + 'AssumedRoleId' => 'test_user_621903f1f21f5.01530789', + 'Arn' => $roleArn + ] + ]; + $stsClient = new StsClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + return Create::promiseFor(new Result($result)); + } + ]); + $credentials = CredentialProvider::assumeRoleWithWebIdentityCredentialProvider([ + 'stsClient' => $stsClient + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => $credentials, + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array( + MetricsBuilder::CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN, + $metrics + ) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric from sts assume role credentials. + * + * @return void + */ + public function testUserAgentCaptureCredentialsStsAssumeRoleMetric() + { + $stsClient = new StsClient([ + 'region' => 'us-east-2', + 'handler' => function ($command, $request) { + return Create::promiseFor( + new Result([ + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'foo' + ] + ]) + ); + } + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => CredentialProvider::assumeRole([ + 'assume_role_params' => [ + 'RoleArn' => 'arn:aws:iam::account-id:role/role-name', + 'RoleSessionName' => 'foo_session' + ], + 'client' => $stsClient + ]), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_STS_ASSUME_ROLE, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric from sts assume role with web identity + * but not sourced from either env vars or profile. + * + * @return void + */ + public function testUserAgentCaptureCredentialsStsAssumeRoleWebIdMetric() + { + $tokenPath = $this->awsDir . '/my-token.jwt'; + file_put_contents($tokenPath, 'token'); + $stsClient = new StsClient([ + 'region' => 'us-east-2', + 'handler' => function ($command, $request) { + return Create::promiseFor( + new Result([ + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'foo' + ] + ]) + ); + } + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => new AssumeRoleWithWebIdentityCredentialProvider([ + 'RoleArn' => 'arn:aws:iam::account-id:role/role-name', + 'RoleSessionName' => 'foo_session', + 'WebIdentityTokenFile' => $tokenPath, + 'client' => $stsClient + ]), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_STS_ASSUME_ROLE_WEB_ID, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric from web id token defined by profile. + * + * @runTestsInSeparateProcesses + * + * @return void + * + * @throws \Exception + */ + public function testUserAgentCaptureCredentialsProfileStsWebIdTokenMetric() + { + $tokenPath = $this->awsDir . '/my-token.jwt'; + $configPath = $this->awsDir . '/my-config'; + file_put_contents($tokenPath, 'token'); + $roleArn = 'arn:aws:iam::123456789012:role/role_name'; + $profileContent = << [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'bar', + 'SessionToken' => 'baz', + 'Expiration' => DateTimeResult::fromEpoch(time() + 10) + ], + 'AssumedRoleUser' => [ + 'AssumedRoleId' => 'test_user_621903f1f21f5.01530789', + 'Arn' => $roleArn + ] + ]; + $stsClient = new StsClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + return Create::promiseFor(new Result($result)); + } + ]); + $credentials = CredentialProvider::assumeRoleWithWebIdentityCredentialProvider([ + 'stsClient' => $stsClient, + 'filename' => $configPath + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => $credentials, + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array( + MetricsBuilder::CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN, + $metrics + ) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Helper method to clean up temporary dirs. + * + * @param $dirPath + * + * @return void + */ + private function cleanUpDir($dirPath): void + { + if (!is_dir($dirPath)) { + return; + } + + $files = dir_iterator($dirPath); + foreach ($files as $file) { + if (in_array($file, ['.', '..'])) { + continue; + } + + $filePath = $dirPath . '/' . $file; + if (is_file($filePath) || !is_dir($filePath)) { + unlink($filePath); + } elseif (is_dir($filePath)) { + $this->cleanUpDir($filePath); + } + } + + rmdir($dirPath); + } + + /** + * Test user agent captures metric for credentials resolved from + * a profile. + * + * @return void + */ + public function testUserAgentCaptureCredentialsProfileMetric() + { + putenv('AWS_PROFILE=default'); + putenv('AWS_PROFILE=default'); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_PROFILE, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric for credentials resolved from IMDS. + * + * @return void + */ + public function testUserAgentCaptureCredentialsIMDSMetric() + { + $imdsCredentials = CredentialProvider::instanceProfile([ + 'client' => $this->imdsTestHandler() + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => $imdsCredentials, + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_IMDS, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Creates a test IMDS http handler to mock request/response to/from IMDS. + * + * @return callable + */ + private function imdsTestHandler(): callable + { + return function (RequestInterface $request) { + $expiration = time() + 1000; + if ($request->getMethod() === 'PUT' && $request->getUri()->getPath() === '/latest/api/token') { + return Create::promiseFor(new Response(200, [], Utils::streamFor(''))); + } elseif ($request->getMethod() === 'GET') { + switch ($request->getUri()->getPath()) { + case '/latest/meta-data/iam/security-credentials/': + return Create::promiseFor(new Response(200, [], Utils::streamFor('MockProfile'))); + case '/latest/meta-data/iam/security-credentials/MockProfile': + $jsonResponse = << new \Exception('Unexpected error!')]); + }; + } + + /** + * Test user agent captures metric for credentials resolved from ECS. + * + * @return void + */ + public function testUserAgentCaptureCredentialsHTTPMetric() + { + $ecsCredentials = CredentialProvider::ecsCredentials([ + 'client' => $this->ecsTestHandler() + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => $ecsCredentials, + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_HTTP, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Creates a test ECS http handler to mock request/response to/from ECS. + * + * @return callable + */ + private function ecsTestHandler(): callable + { + return function (RequestInterface $_) { + $expiration = time() + 1000; + $jsonResponse = <<awsDir . '/my-config'; + $profileContent = << 'us-east-2', + 'credentials' => CredentialProvider::process($profile, $configPath), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_PROCESS, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric for credentials sourced from sso. + * + * @return void + */ + public function testUserAgentCaptureCredentialsSSOMetric() + { + $expiration = time() + 1000; + $ini = <<awsDir . '/my-config'; + file_put_contents($configPath, $ini); + + $tokenFileDir = $this->awsDir . "/sso/cache/"; + if (!is_dir($tokenFileDir)) { + mkdir($tokenFileDir, 0777, true); + } + + putenv('HOME=' . $this->tempDir); + + $tokenLocation = SsoTokenProvider::getTokenLocation('TestSession'); + if (!is_dir(dirname($tokenLocation))) { + mkdir(dirname($tokenLocation), 0777, true); + } + file_put_contents( + $tokenLocation, $tokenFile + ); + $result = [ + 'roleCredentials' => [ + 'accessKeyId' => 'Foo', + 'secretAccessKey' => 'Bazz', + 'sessionToken' => null, + 'expiration' => $expiration + ], + ]; + $ssoClient = new SSOClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + + return Create::promiseFor(new Result($result)); + } + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => CredentialProvider::sso( + 'default', + $configPath, + [ + 'ssoClient' => $ssoClient + ] + ), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_SSO, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } + + /** + * Test user agent captures metric for credentials sourced from sso legacy. + * + * @return void + */ + public function testUserAgentCaptureCredentialsSSOLegacyMetric() + { + $expiration = time() + 1000; + $ini = <<awsDir . '/my-config'; + file_put_contents($configPath, $ini); + + $tokenFileDir = $this->awsDir . "/sso/cache/"; + if (!is_dir($tokenFileDir)) { + mkdir($tokenFileDir, 0777, true); + } + + $tokenFileName = $tokenFileDir . sha1("testssosession.url.com") . '.json'; + file_put_contents( + $tokenFileName, $tokenFile + ); + + putenv('HOME=' . $this->tempDir); + + $result = [ + 'roleCredentials' => [ + 'accessKeyId' => 'Foo', + 'secretAccessKey' => 'Bazz', + 'sessionToken' => null, + 'expiration' => $expiration + ], + ]; + $ssoClient = new SSOClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + + return Create::promiseFor(new Result($result)); + } + ]); + $s3Client = new S3Client([ + 'region' => 'us-east-2', + 'credentials' => CredentialProvider::sso( + 'default', + $configPath, + [ + 'ssoClient' => $ssoClient + ] + ), + 'http_handler' => function ( + RequestInterface $request + ) { + $metrics = $this->getMetricsAsArray($request); + + $this->assertTrue( + in_array(MetricsBuilder::CREDENTIALS_SSO_LEGACY, $metrics) + ); + + return new Response( + 200 + ); + } + ]); + $s3Client->listBuckets(); + } }