From 8e49c6f60c80fa74d42ec054776fb7311d643ad8 Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Fri, 20 Sep 2024 08:59:24 -0700 Subject: [PATCH 1/5] enhancement: user agent 2.1 This change provides: - A builder class for appending metrics - Default initialization of a metrics builder within command instantiation - Wraps user agent logic into a single middleware class - Adds middlewares into the different features from where metrics can be gather, to acomplish this purpose. --- src/AwsClient.php | 22 +- src/ClientResolver.php | 84 +---- src/Command.php | 17 +- src/CommandInterface.php | 7 + src/EndpointV2/EndpointV2Middleware.php | 29 +- src/MetricsBuilder.php | 168 ++++++++++ src/ResultPaginator.php | 4 + src/S3/ApplyChecksumMiddleware.php | 36 ++ src/S3/Crypto/S3EncryptionClient.php | 6 +- src/S3/Crypto/S3EncryptionClientV2.php | 6 +- src/S3/Transfer.php | 5 + src/UserAgentMiddleware.php | 327 +++++++++++++++++++ src/Waiter.php | 4 + tests/AwsClientTest.php | 44 ++- tests/ClientResolverTest.php | 260 --------------- tests/CommandTest.php | 32 +- tests/MetricsBuilderTest.php | 122 +++++++ tests/MetricsBuilderTestTrait.php | 23 ++ tests/MultiRegionClientTest.php | 8 +- tests/ResultPaginatorTest.php | 23 ++ tests/S3/Crypto/S3EncryptionClientTest.php | 16 +- tests/S3/Crypto/S3EncryptionClientV2Test.php | 15 +- tests/S3/TransferTest.php | 10 +- tests/UserAgentMiddlewareTest.php | 277 ++++++++++++++++ tests/WaiterTest.php | 114 +++++-- 25 files changed, 1258 insertions(+), 401 deletions(-) create mode 100644 src/MetricsBuilder.php create mode 100644 src/UserAgentMiddleware.php create mode 100644 tests/MetricsBuilderTest.php create mode 100644 tests/MetricsBuilderTestTrait.php create mode 100644 tests/UserAgentMiddlewareTest.php diff --git a/src/AwsClient.php b/src/AwsClient.php index c58b06c110..2da7d6bef0 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -282,6 +282,11 @@ public function __construct(array $args) if (isset($args['with_resolved'])) { $args['with_resolved']($config); } + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->getHandlerList(), + MetricsBuilder::RESOURCE_MODEL + ); + $this->addUserAgentMiddleware($config); } public function getHandlerList() @@ -439,6 +444,7 @@ private function addSignatureMiddleware(array $args) $name = $this->config['signing_name']; $region = $this->config['signing_region']; $signingRegionSet = $this->signingRegionSet; + $handlerList = $this->getHandlerList(); if (isset($args['signature_version']) || isset($this->config['configured_signature_version']) @@ -457,7 +463,8 @@ private function addSignatureMiddleware(array $args) $region, $signatureVersion, $configuredSignatureVersion, - $signingRegionSet + $signingRegionSet, + $handlerList ) { if (!$configuredSignatureVersion) { if (!empty($c['@context']['signing_region'])) { @@ -492,6 +499,11 @@ private function addSignatureMiddleware(array $args) $region = $signingRegionSet ?? $commandSigningRegionSet ?? $region; + + MetricsBuilder::appendMetricsCaptureMiddleware( + $handlerList, + MetricsBuilder::SIGV4A_SIGNING + ); } return SignatureProvider::resolve($provider, $signatureVersion, $name, $region); @@ -611,6 +623,14 @@ private function addEndpointV2Middleware() ); } + private function addUserAgentMiddleware($args) + { + $this->getHandlerList()->prependSign( + UserAgentMiddleware::wrap($args), + 'user-agent' + ); + } + /** * Retrieves client context param definition from service model, * creates mapping of client context param names with client-provided diff --git a/src/ClientResolver.php b/src/ClientResolver.php index 02f5270f14..ee5a1113cc 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -980,66 +980,8 @@ public static function _default_app_id(array $args) public static function _apply_user_agent($inputUserAgent, array &$args, HandlerList $list) { - // Add SDK version - $userAgent = ['aws-sdk-php/' . Sdk::VERSION]; - - // User Agent Metadata - $userAgent[] = 'ua/2.0'; - - // If on HHVM add the HHVM version - if (defined('HHVM_VERSION')) { - $userAgent []= 'HHVM/' . HHVM_VERSION; - } - - // Add OS version - $disabledFunctions = explode(',', ini_get('disable_functions')); - if (function_exists('php_uname') - && !in_array('php_uname', $disabledFunctions, true) - ) { - $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); - if (!empty($osName)) { - $userAgent []= $osName; - } - } - - // Add the language version - $userAgent []= 'lang/php#' . phpversion(); - - // Add exec environment if present - if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { - $userAgent []= $executionEnvironment; - } - // Add endpoint discovery if set - if (isset($args['endpoint_discovery'])) { - if (($args['endpoint_discovery'] instanceof \Aws\EndpointDiscovery\Configuration - && $args['endpoint_discovery']->isEnabled()) - ) { - $userAgent []= 'cfg/endpoint-discovery'; - } elseif (is_array($args['endpoint_discovery']) - && isset($args['endpoint_discovery']['enabled']) - && $args['endpoint_discovery']['enabled'] - ) { - $userAgent []= 'cfg/endpoint-discovery'; - } - } - - // Add retry mode if set - if (isset($args['retries'])) { - if ($args['retries'] instanceof \Aws\Retry\Configuration) { - $userAgent []= 'cfg/retry-mode#' . $args["retries"]->getMode(); - } elseif (is_array($args['retries']) - && isset($args["retries"]["mode"]) - ) { - $userAgent []= 'cfg/retry-mode#' . $args["retries"]["mode"]; - } - } - - // AppID Metadata - if (!empty($args['app_id'])) { - $userAgent[] = 'app/' . $args['app_id']; - } - + $userAgent = []; // Add the input to the end if ($inputUserAgent){ if (!is_array($inputUserAgent)) { @@ -1050,30 +992,6 @@ public static function _apply_user_agent($inputUserAgent, array &$args, HandlerL } $args['ua_append'] = $userAgent; - - $list->appendBuild(static function (callable $handler) use ($userAgent) { - return function ( - CommandInterface $command, - RequestInterface $request - ) use ($handler, $userAgent) { - return $handler( - $command, - $request->withHeader( - 'X-Amz-User-Agent', - implode(' ', array_merge( - $userAgent, - $request->getHeader('X-Amz-User-Agent') - )) - )->withHeader( - 'User-Agent', - implode(' ', array_merge( - $userAgent, - $request->getHeader('User-Agent') - )) - ) - ); - }; - }); } public static function _apply_endpoint($value, array &$args, HandlerList $list) diff --git a/src/Command.php b/src/Command.php index 4c0a9c54ea..3304fb5f05 100644 --- a/src/Command.php +++ b/src/Command.php @@ -14,9 +14,12 @@ class Command implements CommandInterface /** @var HandlerList */ private $handlerList; - /** @var Array */ + /** @var array */ private $authSchemes; + /** @var MetricsBuilder */ + private $metricsBuilder; + /** * Accepts an associative array of command options, including: * @@ -38,6 +41,7 @@ public function __construct($name, array $args = [], ?HandlerList $list = null) if (!isset($this->data['@context'])) { $this->data['@context'] = []; } + $this->metricsBuilder = new MetricsBuilder(); } public function __clone() @@ -110,4 +114,15 @@ public function get($name) { return $this[$name]; } + + /** + * @inheridoc + * @internal + * + * @return MetricsBuilder + */ + public function getMetricsBuilder(): MetricsBuilder + { + return $this->metricsBuilder; + } } diff --git a/src/CommandInterface.php b/src/CommandInterface.php index b35c75d37b..853338039a 100644 --- a/src/CommandInterface.php +++ b/src/CommandInterface.php @@ -39,4 +39,11 @@ public function hasParam($name); * @return HandlerList */ public function getHandlerList(); + + /** + * Returns the metrics builder instance tied up to this command. + * + * @return MetricsBuilder + */ + public function getMetricsBuilder(); } diff --git a/src/EndpointV2/EndpointV2Middleware.php b/src/EndpointV2/EndpointV2Middleware.php index fc8861e109..b9c970d238 100644 --- a/src/EndpointV2/EndpointV2Middleware.php +++ b/src/EndpointV2/EndpointV2Middleware.php @@ -5,6 +5,7 @@ use Aws\Api\Service; use Aws\Auth\Exception\UnresolvedAuthSchemeException; use Aws\CommandInterface; +use Aws\MetricsBuilder; use Closure; use GuzzleHttp\Promise\Promise; use function JmesPath\search; @@ -98,8 +99,15 @@ 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 + ); $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs); - + $this->hookAccountIdEndpointMetric( + $endpoint, + $command + ); if (!empty($authSchemes = $endpoint->getProperty('authSchemes'))) { $this->applyAuthScheme( $authSchemes, @@ -394,4 +402,23 @@ 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 new file mode 100644 index 0000000000..4bb803fb84 --- /dev/null +++ b/src/MetricsBuilder.php @@ -0,0 +1,168 @@ +metrics = []; + // The first metrics does not include the separator + // therefore it is reduced by default. + $this->metricsSize = -(strlen(self::$METRIC_SEPARATOR)); + } + + /** + * Build the metrics string value. + * + * @return string + */ + public function build(): string + { + if (empty($this->metrics)) { + return ""; + } + + return $this->encode(); + } + + /** + * Encodes the metrics by separating each metric + * with a comma. Example: for the metrics[A,B,C] then + * the output would be "A,B,C". + * + * @return string + */ + private function encode(): string + { + return implode(self::$METRIC_SEPARATOR, array_keys($this->metrics)); + } + + /** + * 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. + * Example: $currentSize = $currentSize + len($newMetric) + len($separator) + * + * @param string $metric + * + * @return void + */ + public function append(string $metric): void + { + if (!$this->canMetricBeAppended($metric)) { + return; + } + + $this->metrics[$metric] = true; + $this->metricsSize += strlen($metric) + strlen(self::$METRIC_SEPARATOR); + } + + /** + * 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 + * and: + * $newMetric not in $existentMetrics + * + * @param string $newMetric + * + * @return bool + */ + private function canMetricBeAppended(string $newMetric): bool + { + if ($this->metricsSize + + (strlen($newMetric) + strlen(self::$METRIC_SEPARATOR)) + > self::$MAX_METRICS_SIZE + ) { + @trigger_error( + "The metric `{$newMetric}` " + . "can not be added due to size constraints", + E_USER_WARNING + ); + + return false; + } + + if (isset($this->metrics[$newMetric])) { + @trigger_error( + 'The metric ' . $newMetric. ' is already appended!', + E_USER_WARNING + ); + + return false; + } + + return true; + } + + /** + * Returns the metrics builder from the property @context of a command. + * + * @param Command $command + * + * @return MetricsBuilder + */ + public static function fromCommand(CommandInterface $command): MetricsBuilder + { + return $command->getMetricsBuilder(); + } + + public static function appendMetricsCaptureMiddleware( + HandlerList $handlerList, + $metric + ): void + { + $handlerList->appendBuild( + Middleware::tap( + function (CommandInterface $command) use ($metric) { + self::fromCommand($command)->append( + $metric + ); + } + ), + 'metrics-capture-'.$metric + ); + } +} diff --git a/src/ResultPaginator.php b/src/ResultPaginator.php index c9c3bd293d..396ed1a936 100644 --- a/src/ResultPaginator.php +++ b/src/ResultPaginator.php @@ -45,6 +45,10 @@ public function __construct( $this->operation = $operation; $this->args = $args; $this->config = $config; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::PAGINATOR + ); } /** diff --git a/src/S3/ApplyChecksumMiddleware.php b/src/S3/ApplyChecksumMiddleware.php index a0ff65d6dc..ba79d8b1aa 100644 --- a/src/S3/ApplyChecksumMiddleware.php +++ b/src/S3/ApplyChecksumMiddleware.php @@ -3,6 +3,7 @@ use Aws\Api\Service; use Aws\CommandInterface; +use Aws\MetricsBuilder; use GuzzleHttp\Psr7; use InvalidArgumentException; use Psr\Http\Message\RequestInterface; @@ -82,6 +83,9 @@ public function __invoke( . implode(", ", $supportedAlgorithms) . "." ); } + + $this->hookChecksumAlgorithmMetric($requestedAlgorithm, $command); + return $next($command, $request); } @@ -94,6 +98,7 @@ 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); } elseif (!$request->hasHeader('Content-MD5')) { // Set the content MD5 header for operations that require it. $request = $request->withHeader( @@ -111,6 +116,7 @@ public function __invoke( 'X-Amz-Content-Sha256', $command['ContentSHA256'] ); + $this->hookChecksumAlgorithmMetric('sha256', $command); } return $next($command, $request); @@ -144,4 +150,34 @@ 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/S3/Crypto/S3EncryptionClient.php b/src/S3/Crypto/S3EncryptionClient.php index 30b51007bc..96cb152a34 100644 --- a/src/S3/Crypto/S3EncryptionClient.php +++ b/src/S3/Crypto/S3EncryptionClient.php @@ -3,6 +3,7 @@ use Aws\Crypto\DecryptionTrait; use Aws\HashingStream; +use Aws\MetricsBuilder; use Aws\PhpHash; use Aws\Crypto\AbstractCryptoClient; use Aws\Crypto\EncryptionTrait; @@ -53,9 +54,12 @@ public function __construct( S3Client $client, $instructionFileSuffix = null ) { - $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $this->instructionFileSuffix = $instructionFileSuffix; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_CRYPTO_V1N + ); } private static function getDefaultStrategy() diff --git a/src/S3/Crypto/S3EncryptionClientV2.php b/src/S3/Crypto/S3EncryptionClientV2.php index 5690c76dd9..fe917800f7 100644 --- a/src/S3/Crypto/S3EncryptionClientV2.php +++ b/src/S3/Crypto/S3EncryptionClientV2.php @@ -4,6 +4,7 @@ use Aws\Crypto\DecryptionTraitV2; use Aws\Exception\CryptoException; use Aws\HashingStream; +use Aws\MetricsBuilder; use Aws\PhpHash; use Aws\Crypto\AbstractCryptoClientV2; use Aws\Crypto\EncryptionTraitV2; @@ -105,10 +106,13 @@ public function __construct( S3Client $client, $instructionFileSuffix = null ) { - $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $this->instructionFileSuffix = $instructionFileSuffix; $this->legacyWarningCount = 0; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_CRYPTO_V2 + ); } private static function getDefaultStrategy() diff --git a/src/S3/Transfer.php b/src/S3/Transfer.php index 600f441008..1beb7473e9 100644 --- a/src/S3/Transfer.php +++ b/src/S3/Transfer.php @@ -4,6 +4,7 @@ use Aws; use Aws\CommandInterface; use Aws\Exception\AwsException; +use Aws\MetricsBuilder; use GuzzleHttp\Promise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Promise\PromisorInterface; @@ -139,6 +140,10 @@ public function __construct( // Handle "add_content_md5" option. $this->addContentMD5 = isset($options['add_content_md5']) && $options['add_content_md5'] === true; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_TRANSFER + ); } /** diff --git a/src/UserAgentMiddleware.php b/src/UserAgentMiddleware.php new file mode 100644 index 0000000000..d18eab309e --- /dev/null +++ b/src/UserAgentMiddleware.php @@ -0,0 +1,327 @@ +nextHandler = $nextHandler; + $this->args = $args; + } + + /** + * When invoked, its injects the user agent header into the + * request headers. + * @param CommandInterface $command + * @param RequestInterface $request + * + * @return mixed + */ + public function __invoke(CommandInterface $command, RequestInterface $request) + { + $handler = $this->nextHandler; + $this->metricsBuilder = MetricsBuilder::fromCommand($command); + $request = $this->requestWithUserAgentHeader($request); + + return $handler($command, $request); + } + + /** + * Builds the user agent header value, and injects it into the request + * headers. Then, it returns the mutated request. + * + * @param RequestInterface $request + * + * @return RequestInterface + */ + private function requestWithUserAgentHeader(RequestInterface $request): RequestInterface + { + $uaAppend = $this->args['ua_append'] ?? []; + $userAgentValue = array_merge( + $this->buildUserAgentValue(), + $uaAppend + ); + // It includes the user agent values just for the User-Agent header. + // The reason is that the SEP does not mention appending the + // metrics into the X-Amz-User-Agent header. + return $request->withHeader( + 'X-Amz-User-Agent', + implode(' ', array_merge( + $uaAppend, + $request->getHeader('X-Amz-User-Agent') + )) + )->withHeader( + 'User-Agent', + implode(' ', array_merge( + $userAgentValue, + $request->getHeader('User-Agent') + )) + ); + } + + /** + * Builds the different user agent values. + * + * @return array + */ + private function buildUserAgentValue(): array + { + static $fnList = [ + 'sdkVersion', + 'userAgentVersion', + 'hhvmVersion', + 'osName', + 'langVersion', + 'execEnv', + 'endpointDiscovery', + 'appId', + 'metrics' + ]; + $userAgentValue = []; + foreach ($fnList as $fn) { + $val = $this->{$fn}(); + if (!empty($val)) { + $userAgentValue[] = $val; + } + } + + return $userAgentValue; + } + + /** + * Returns the user agent value for SDK version. + * + * @return string + */ + private function sdkVersion(): string + { + return 'aws-sdk-php/' . Sdk::VERSION; + } + + /** + * Returns the user agent value for the agent version. + * + * @return string + */ + private function userAgentVersion(): string + { + return 'ua/' . self::AGENT_VERSION; + } + + /** + * Returns the user agent value for the hhvm version, but just + * when it is defined. + * + * @return string + */ + private function hhvmVersion(): string + { + if (defined('HHVM_VERSION')) { + return 'HHVM/' . HHVM_VERSION; + } + + return ""; + } + + /** + * Returns the user agent value for the os version. + * + * @return string + */ + private function osName(): string + { + $disabledFunctions = explode(',', ini_get('disable_functions')); + if (function_exists('php_uname') + && !in_array('php_uname', $disabledFunctions, true) + ) { + $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); + if (!empty($osName)) { + return $osName; + } + } + + return ""; + } + + /** + * Returns the user agent value for the php language used. + * + * @return string + */ + private function langVersion(): string + { + return 'lang/php#' . phpversion(); + } + + /** + * Returns the user agent value for the execution env. + * + * @return string + */ + private function execEnv(): string + { + if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { + return $executionEnvironment; + } + + return ""; + } + + private function endpointDiscovery(): string + { + $args = $this->args; + if (isset($args['endpoint_discovery'])) { + if (($args['endpoint_discovery'] instanceof Configuration + && $args['endpoint_discovery']->isEnabled()) + ) { + return 'cfg/endpoint-discovery'; + } elseif (is_array($args['endpoint_discovery']) + && isset($args['endpoint_discovery']['enabled']) + && $args['endpoint_discovery']['enabled'] + ) { + return 'cfg/endpoint-discovery'; + } + } + + return ""; + } + + /** + * Returns the user agent value for app id, but just when an + * app id was provided as a client argument. + * + * @return string + */ + private function appId(): string + { + if (empty($this->args['app_id'])) { + return ""; + } + + return 'app/' . $this->args['app_id']; + } + + /** + * Returns the user agent value for metrics. + * + * @return string + */ + private function metrics(): string + { + static $metricsFn = [ + 'endpointMetric', + 'accountIdModeMetric', + 'retryConfigMetric' + ]; + foreach ($metricsFn as $fn) { + $this->{$fn}(); + } + + $metricsEncoded = $this->metricsBuilder->build(); + if (empty($metricsEncoded)) { + return ""; + } + + return "m/" . $metricsEncoded; + } + + /** + * Appends the endpoint metric into the metrics builder, + * just if a custom endpoint was provided at client construction. + */ + private function endpointMetric(): void + { + if (!empty($this->args['endpoint'])) { + $this->metricsBuilder->append(MetricsBuilder::ENDPOINT_OVERRIDE); + } + } + + /** + * Appends the account id endpoint mode metric into the metrics builder, + * based on the account id endpoint mode provide as client argument. + */ + private function accountIdModeMetric(): void + { + $accountIdMode = $this->args['account_id_endpoint_mode'] ?? null; + if ($accountIdMode === null) { + return; + } + + if ($accountIdMode === 'preferred') { + $this->metricsBuilder->append(MetricsBuilder::ACCOUNT_ID_MODE_PREFERRED); + } elseif ($accountIdMode === 'disabled') { + $this->metricsBuilder->append(MetricsBuilder::ACCOUNT_ID_MODE_DISABLED); + } elseif ($accountIdMode === 'required') { + $this->metricsBuilder->append(MetricsBuilder::ACCOUNT_ID_MODE_REQUIRED); + } + } + + /** + * Appends the retry mode metric into the metrics builder, + * based on the resolved retry config mode. + */ + private function retryConfigMetric(): void + { + $retries = $this->args['retries'] ?? null; + if ($retries === null) { + return; + } + + $retryMode = ''; + if ($retries instanceof \Aws\Retry\Configuration) { + $retryMode = $retries->getMode(); + } elseif (is_array($retries) + && isset($retries["mode"]) + ) { + $retryMode = $retries["mode"]; + } + + if ($retryMode === 'legacy') { + $this->metricsBuilder->append( + MetricsBuilder::RETRY_MODE_LEGACY + ); + } elseif ($retryMode === 'standard') { + $this->metricsBuilder->append( + MetricsBuilder::RETRY_MODE_STANDARD + ); + } elseif ($retryMode === 'adaptive') { + $this->metricsBuilder->append( + MetricsBuilder::RETRY_MODE_ADAPTIVE + ); + } + } +} diff --git a/src/Waiter.php b/src/Waiter.php index 16b86fb2fb..3310e8e3b7 100644 --- a/src/Waiter.php +++ b/src/Waiter.php @@ -85,6 +85,10 @@ public function __construct( 'The provided "before" callback is not callable.' ); } + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::WAITER + ); } /** diff --git a/tests/AwsClientTest.php b/tests/AwsClientTest.php index 0052f66217..444dd5f827 100644 --- a/tests/AwsClientTest.php +++ b/tests/AwsClientTest.php @@ -10,6 +10,7 @@ use Aws\Endpoint\UseFipsEndpoint\Configuration as FipsConfiguration; use Aws\Endpoint\UseDualStackEndpoint\Configuration as DualStackConfiguration; use Aws\EndpointV2\EndpointProviderV2; +use Aws\MetricsBuilder; use Aws\Middleware; use Aws\ResultPaginator; use Aws\S3\Exception\S3Exception; @@ -22,8 +23,8 @@ use Aws\Token\Token; use Aws\Waiter; use Aws\WrappedHttpHandler; -use Exception; use GuzzleHttp\Promise\RejectedPromise; +use GuzzleHttp\Psr7\Response; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -34,6 +35,7 @@ class AwsClientTest extends TestCase { use UsesServiceTrait; use TestServiceTrait; + use MetricsBuilderTestTrait; private function getApiProvider() { @@ -990,10 +992,44 @@ public function testQueryModeHeaderAdded(): void $client = $this->generateTestClient($service); $list = $client->getHandlerList(); $list->setHandler(new MockHandler([new Result()])); - $list->appendSign(Middleware::tap(function($cmd, $req) { - $this->assertTrue($req->hasHeader('x-amzn-query-mode')); - $this->assertEquals(true, $req->getHeaderLine('x-amzn-query-mode')); + $list->appendSign(Middleware::tap(function ($cmd, $req) { + $this->assertTrue($req->hasHeader('x-amzn-query-mode')); + $this->assertEquals(true, $req->getHeaderLine('x-amzn-query-mode')); })); $client->TestOperation(); } + + 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([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $userAgentValue = $request->getHeaderLine('User-Agent'); + + $this->assertNotEmpty($userAgentValue); + + return new Response(); + } + ]); + $client->listBuckets(); + } } diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index 638fabfc4f..422b4655c2 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -830,268 +830,8 @@ public function testAppliesUserAgent() $this->assertArrayHasKey('ua_append', $conf); $this->assertIsArray($conf['ua_append']); $this->assertContains('PHPUnit/Unit', $conf['ua_append']); - $this->assertContains('aws-sdk-php/' . Sdk::VERSION, $conf['ua_append']); - } - - public function testUserAgentAlwaysStartsWithSdkAgentString() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* MockBuilder/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* MockBuilder/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = []; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - public function testUserAgentAddsEndpointDiscoveryConfiguration() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'endpoint_discovery' => new \Aws\EndpointDiscovery\Configuration ( - true, - 1000 - ), - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - - public function testUserAgentAddsEndpointDiscoveryArray() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'endpoint_discovery' => [ - 'enabled' => true, - 'cache_limit' => 1000 - ], - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - public function testUserAgentAddsRetryModeConfiguration() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#adaptive/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#adaptive/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'retries' => new \Aws\Retry\Configuration('adaptive', 10) - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - - public function testUserAgentAddsRetryWithArray() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#standard/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#standard/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'retries' => [ - 'mode' => 'standard', - ], - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); } - /** * @dataProvider statValueProvider * @param bool|array $userValue diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 08d21d7106..df2bcb444e 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -3,6 +3,7 @@ use Aws\Command; use Aws\HandlerList; +use Aws\MetricsBuilder; use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** @@ -45,20 +46,28 @@ public function testHasGetMethod() public function testIsIterable() { - $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); - $data = iterator_to_array($c); - $this->assertEquals( - ['bar' => 'baz', 'qux' => 'boo', '@http' => [], '@context' => []], + $command = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); + $data = iterator_to_array($command); + $this->assertEquals([ + 'bar' => 'baz', + 'qux' => 'boo', + '@http' => [], + '@context' => [] + ], $data ); } public function testConvertToArray() { - $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); - $this->assertEquals( - ['bar' => 'baz', 'qux' => 'boo', '@http' => [], '@context' => []], - $c->toArray() + $command = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); + $this->assertEquals([ + 'bar' => 'baz', + 'qux' => 'boo', + '@http' => [], + '@context' => [] + ], + $command->toArray() ); } @@ -109,4 +118,11 @@ public function testSetAuthSchemesEmitsWarning() $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); $c->setAuthSchemes([]); } + + public function testInitializeMetricsBuilderObject() + { + $command = new Command('Foo', []); + $metricsBuilder = MetricsBuilder::fromCommand($command); + $this->assertInstanceOf(MetricsBuilder::class, $metricsBuilder); + } } diff --git a/tests/MetricsBuilderTest.php b/tests/MetricsBuilderTest.php new file mode 100644 index 0000000000..99c9e23cd4 --- /dev/null +++ b/tests/MetricsBuilderTest.php @@ -0,0 +1,122 @@ +append(chr($char)); + $expectedMetrics[] = chr($char); + } + + $this->assertEquals( + implode(',', $expectedMetrics), + $metricsBuilder->build() + ); + } + + public function testEncodeMetrics() + { + $metricsBuilder = new MetricsBuilder(); + $expectedMetrics = "A,B,C"; // encoding format + $metricsBuilder->append("A"); + $metricsBuilder->append("B"); + $metricsBuilder->append("C"); + + $this->assertEquals( + $expectedMetrics, + $metricsBuilder->build() + ); + } + + public function testConstraintsAppendToMetricsSize() + { + try { + set_error_handler( + static function ( $errno, $errstr ) { + // Mute warning + }, + E_ALL + ); + $metricsBuilder = new MetricsBuilder(); + $firstMetric = str_repeat("*", 1024); + $metricsBuilder->append($firstMetric); + $metricsBuilder->append("A"); + $metricsBuilder->append("B"); + + $this->assertEquals($firstMetric, $metricsBuilder->build()); + } finally { + restore_error_handler(); + } + } + + public function testEmitMetricsSizeConstraintWarning() + { + try { + // Prevent deprecation warning for expectWarning + set_error_handler( + static function ( $errno, $errstr ) { + throw new \Exception( $errstr, $errno ); + }, + E_ALL + ); + $this->expectException(Exception::class); + $this->expectExceptionMessage( + "The metric `A` can not be added due to size constraints" + ); + $metricsBuilder = new MetricsBuilder(); + $firstMetric = str_repeat("*", 1024); + $metricsBuilder->append($firstMetric); + $metricsBuilder->append("A"); + } finally { + restore_error_handler(); + } + } + + public function testGetMetricsBuilderFromCommand() + { + $command = new Command('TestCommand', [], new HandlerList()); + $metricsBuilder = MetricsBuilder::fromCommand($command); + $this->assertInstanceOf( MetricsBuilder::class, $metricsBuilder); + } + + public function testAppendMetricsCaptureMiddleware() + { + $handlerList = new HandlerList(function (){}); + $metric = "Foo"; + // It should be appended into the build step + MetricsBuilder::appendMetricsCaptureMiddleware( + $handlerList, + "$metric" + ); + // The sign step is ahead of the build step + // which means we should catch the metric appended + // previously. + $handlerList->appendSign(Middleware::tap( + function ( + CommandInterface $command + ) use ($metric) { + $metricsBuilder = MetricsBuilder::fromCommand($command); + + $this->assertEquals( + $metric, + $metricsBuilder->build() + ); + } + )); + $handlerFn = $handlerList->resolve(); + $command = new Command('Buzz', []); + $handlerFn($command); + } +} diff --git a/tests/MetricsBuilderTestTrait.php b/tests/MetricsBuilderTestTrait.php new file mode 100644 index 0000000000..82f322d2e8 --- /dev/null +++ b/tests/MetricsBuilderTestTrait.php @@ -0,0 +1,23 @@ +getHeaderLine('User-Agent'), + $matches + ) !== false) { + $metrics = $matches[2]; + + return explode(',', $metrics); + } + + return []; + } +} diff --git a/tests/MultiRegionClientTest.php b/tests/MultiRegionClientTest.php index c8913ac9a5..3aad0e47e5 100644 --- a/tests/MultiRegionClientTest.php +++ b/tests/MultiRegionClientTest.php @@ -98,7 +98,13 @@ public function testProxiesArbitraryCallsToRegionalizedClient() }); $this->mockRegionalClient->expects($this->once()) ->method('getCommand') - ->with('baz', ['foo' => 'bar', '@http' => [], '@context' => []]) + ->with( + 'baz', + [ + 'foo' => 'bar', + '@http' => [], + '@context' => [] + ]) ->willReturn(new Command('Baz', [], $mockHandler)); $this->instance->baz(['foo' => 'bar']); diff --git a/tests/ResultPaginatorTest.php b/tests/ResultPaginatorTest.php index c4dc0ad2dc..9cf1e77470 100644 --- a/tests/ResultPaginatorTest.php +++ b/tests/ResultPaginatorTest.php @@ -5,8 +5,11 @@ use Aws\CloudWatchLogs\CloudWatchLogsClient; use Aws\CommandInterface; use Aws\DynamoDb\DynamoDbClient; +use Aws\MetricsBuilder; use Aws\Result; +use Aws\S3\S3Client; use GuzzleHttp\Promise; +use GuzzleHttp\Psr7\Response; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -16,6 +19,7 @@ class ResultPaginatorTest extends TestCase { use UsesServiceTrait; + use MetricsBuilderTestTrait; private function getCustomClientProvider(array $config) { @@ -457,4 +461,23 @@ function () use (&$requestCount) { $this->assertInstanceOf(Result::class, $result); $this->assertEquals(3, $requestCount); } + + public function testAppendsMetricsCaptureMiddleware() + { + $client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $this->assertTrue( + in_array( + MetricsBuilder::PAGINATOR, + $this->getMetricsAsArray($request) + ) + ); + + return new Response(); + } + ]); + $paginator = $client->getPaginator('ListBuckets'); + $paginator->current(); + } } diff --git a/tests/S3/Crypto/S3EncryptionClientTest.php b/tests/S3/Crypto/S3EncryptionClientTest.php index f0f07d201a..0e9fbfe3d9 100644 --- a/tests/S3/Crypto/S3EncryptionClientTest.php +++ b/tests/S3/Crypto/S3EncryptionClientTest.php @@ -2,15 +2,14 @@ namespace Aws\Test\S3\Crypto; use Aws\Crypto\KmsMaterialsProviderV2; +use Aws\MetricsBuilder; use Aws\S3\Crypto\S3EncryptionClient; use Aws\Result; use Aws\HashingStream; -use Aws\Crypto\MaterialsProvider; use Aws\Crypto\AesDecryptingStream; use Aws\Crypto\AesGcmDecryptingStream; use Aws\Crypto\KmsMaterialsProvider; use Aws\Crypto\MetadataEnvelope; -use Aws\S3\Crypto\S3EncryptionClientV2; use Aws\S3\S3Client; use Aws\S3\Crypto\HeadersMetadataStrategy; use Aws\S3\Crypto\InstructionFileMetadataStrategy; @@ -20,6 +19,7 @@ use GuzzleHttp\Promise; use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Psr7\Response; +use Aws\Test\MetricsBuilderTestTrait; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -29,6 +29,7 @@ class S3EncryptionClientTest extends TestCase use UsesCryptoParamsTrait; use UsesMetadataEnvelopeTrait; use UsesServiceTrait; + use MetricsBuilderTestTrait; protected function getS3Client() { @@ -766,7 +767,7 @@ public function testTriggersWarningForGcmEncryptionWithAad() $this->assertTrue($this->mockQueueEmpty()); } - public function testAddsCryptoUserAgent() + public function testAppendsMetricsCaptureMiddleware() { $kms = $this->getKmsClient(); $provider = new KmsMaterialsProvider($kms); @@ -778,10 +779,13 @@ public function testAddsCryptoUserAgent() 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function (RequestInterface $req) use ($provider) { - $this->assertStringContainsString( - 'feat/s3-encrypt/' . S3EncryptionClient::CRYPTO_VERSION, - $req->getHeaderLine('User-Agent') + $this->assertTrue( + in_array( + MetricsBuilder::S3_CRYPTO_V1N, + $this->getMetricsAsArray($req) + ) ); + return Promise\Create::promiseFor(new Response( 200, $this->getFieldsAsMetaHeaders( diff --git a/tests/S3/Crypto/S3EncryptionClientV2Test.php b/tests/S3/Crypto/S3EncryptionClientV2Test.php index 4930182eb0..f76919fac6 100644 --- a/tests/S3/Crypto/S3EncryptionClientV2Test.php +++ b/tests/S3/Crypto/S3EncryptionClientV2Test.php @@ -7,11 +7,13 @@ use Aws\Crypto\KmsMaterialsProviderV2; use Aws\Crypto\MetadataEnvelope; use Aws\HashingStream; +use Aws\MetricsBuilder; use Aws\Result; use Aws\S3\S3Client; use Aws\S3\Crypto\InstructionFileMetadataStrategy; use Aws\S3\Crypto\S3EncryptionClientV2; use Aws\Test\Crypto\UsesCryptoParamsTraitV2; +use Aws\Test\MetricsBuilderTestTrait; use Aws\Test\UsesServiceTrait; use Aws\Test\Crypto\UsesMetadataEnvelopeTrait; use GuzzleHttp\Promise; @@ -26,6 +28,7 @@ class S3EncryptionClientV2Test extends TestCase use UsesCryptoParamsTraitV2; use UsesMetadataEnvelopeTrait; use UsesServiceTrait; + use MetricsBuilderTestTrait; protected function getS3Client() { @@ -1032,22 +1035,24 @@ public function testThrowsForIncorrectSecurityProfile() ]); } - public function testAddsCryptoUserAgent() + public function testAppendsMetricsCaptureMiddleware() { $kms = $this->getKmsClient(); $provider = new KmsMaterialsProviderV2($kms, 'foo'); $this->addMockResults($kms, [ new Result(['Plaintext' => random_bytes(32)]) ]); - $s3 = new S3Client([ 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function (RequestInterface $req) use ($provider) { - $this->assertStringContainsString( - 'feat/s3-encrypt/' . S3EncryptionClientV2::CRYPTO_VERSION, - $req->getHeaderLine('User-Agent') + $this->assertTrue( + in_array( + MetricsBuilder::S3_CRYPTO_V2, + $this->getMetricsAsArray($req) + ) ); + return Promise\Create::promiseFor(new Response( 200, $this->getFieldsAsMetaHeaders( diff --git a/tests/S3/TransferTest.php b/tests/S3/TransferTest.php index 4828888ad9..c6ac8e25d0 100644 --- a/tests/S3/TransferTest.php +++ b/tests/S3/TransferTest.php @@ -2,6 +2,7 @@ namespace Aws\Test\S3; use Aws\CommandInterface; +use Aws\HandlerList; use Aws\Middleware; use Aws\Result; use Aws\S3\S3Client; @@ -417,8 +418,15 @@ private function mockResult(callable $fn) /** @return S3Client|\PHPUnit_Framework_MockObject_MockObject */ private function getMockS3Client() { - return $this->getMockBuilder(S3Client::class) + $mockClient = $this->getMockBuilder(S3Client::class) ->disableOriginalConstructor() ->getMock(); + $mockHandler = $this->getMockBuilder(HandlerList::class) + ->disableOriginalConstructor() + ->getMock(); + $mockClient->method('getHandlerList') + ->willReturn($mockHandler); + + return $mockClient; } } diff --git a/tests/UserAgentMiddlewareTest.php b/tests/UserAgentMiddlewareTest.php new file mode 100644 index 0000000000..c5e64ac9fd --- /dev/null +++ b/tests/UserAgentMiddlewareTest.php @@ -0,0 +1,277 @@ +deferFns) > 0) { + $fn = array_pop($this->deferFns); + $fn(); + } + } + + /** + * Tests the user agent header is appended into the request headers. + * + * @return void + */ + public function testAppendsUserAgentHeader() + { + $handler = UserAgentMiddleware::wrap([]); + $middleware = $handler(function ( + CommandInterface $command, + RequestInterface $request + ) { + $userAgent = $request->getHeaderLine('User-Agent'); + + $this->assertNotEmpty($userAgent); + }); + $request = new Request('post', 'foo', [], 'buzz'); + $middleware(new Command('buzz'), $request); + } + + /** + * Tests the user agent header value contains the expected + * component. + * + * @dataProvider userAgentCasesDataProvider + * @param array $args + * @param string $expected + * + * @return void + */ + public function testUserAgentContainsValue(array $args, string $expected) + { + $handler = UserAgentMiddleware::wrap($args); + $middleware = $handler(function ( + CommandInterface $command, + RequestInterface $request + ) use ($expected) { + if (empty($expected)) { + $this->markTestSkipped('Expected value is empty'); + } + $userAgent = $request->getHeaderLine('User-Agent'); + $userAgentValues = explode(' ', $userAgent); + $this->assertTrue(in_array($expected, $userAgentValues)); + }); + $request = new Request('post', 'foo', [], 'buzz'); + $middleware(new Command('buzz'), $request); + } + + /** + * It returns a generator that yields an argument and an expected value + * per iteration. + * Example: yield [$arguments, 'ExpectedValue'] + * + * @return Generator + */ + public function userAgentCasesDataProvider(): Generator + { + $userAgentCases = [ + 'sdkVersion' => [[], 'aws-sdk-php/' . Sdk::VERSION], + 'userAgentVersion' => [ + [], 'ua/' . UserAgentMiddleware::AGENT_VERSION + ], + 'hhvmVersion' => function (): array { + if (defined('HHVM_VERSION')) { + return [[], 'HHVM/' . HHVM_VERSION]; + } + + return [[], ""]; + }, + 'osName' => function (): array { + $disabledFunctions = explode( + ',', + ini_get('disable_functions') + ); + if (function_exists('php_uname') + && !in_array( + 'php_uname', + $disabledFunctions, + true + ) + ) { + $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); + if (!empty($osName)) { + return [[], $osName]; + } + } + + return [[], ""]; + }, + '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 { + $expectedAppId = "FooAppId"; + $args = [ + 'app_id' => $expectedAppId + ]; + + return [$args, "app/{$expectedAppId}"]; + }, + 'metricsWithEndpoint' => function (): array { + $expectedEndpoint = "https://foo-endpoint.com"; + $args = [ + 'endpoint' => $expectedEndpoint + ]; + + return [$args, 'm/' . MetricsBuilder::ENDPOINT_OVERRIDE]; + }, + 'metricsWithAccountIdModePreferred' => function (): array { + $args = [ + 'account_id_endpoint_mode' => 'preferred' + ]; + + return [$args, 'm/' . MetricsBuilder::ACCOUNT_ID_MODE_PREFERRED]; + }, + 'metricsWithAccountIdModeRequired' => function (): array { + $args = [ + 'account_id_endpoint_mode' => 'required' + ]; + + return [$args, 'm/' . MetricsBuilder::ACCOUNT_ID_MODE_REQUIRED]; + }, + 'metricsWithAccountIdModeDisabled' => function (): array { + $args = [ + 'account_id_endpoint_mode' => 'disabled' + ]; + + return [$args, 'm/' . MetricsBuilder::ACCOUNT_ID_MODE_DISABLED]; + }, + 'metricsWithRetryConfigArrayStandardMode' => function (): array { + $args = [ + 'retries' => [ + 'mode' => 'standard' + ] + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_STANDARD]; + }, + 'metricsWithRetryConfigArrayAdaptiveMode' => function (): array { + $args = [ + 'retries' => [ + 'mode' => 'adaptive' + ] + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_ADAPTIVE]; + }, + 'metricsWithRetryConfigArrayLegacyMode' => function (): array { + $args = [ + 'retries' => [ + 'mode' => 'legacy' + ] + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_LEGACY]; + }, + 'metricsWithRetryConfigStandardMode' => function (): array { + $args = [ + 'retries' => new \Aws\Retry\Configuration( + 'standard', + 10 + ) + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_STANDARD]; + }, + 'metricsWithRetryConfigAdaptiveMode' => function (): array { + $args = [ + 'retries' => new \Aws\Retry\Configuration( + 'adaptive', + 10 + ) + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_ADAPTIVE]; + }, + 'metricsWithRetryConfigLegacyMode' => function (): array { + $args = [ + 'retries' => new \Aws\Retry\Configuration( + 'legacy', + 10 + ) + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_LEGACY]; + }, + 'cfgWithEndpointDiscoveryConfigArray' => function (): array { + $args = [ + 'endpoint_discovery' => [ + 'enabled' => true, + 'cache_limit' => 1000 + ] + ]; + + return [$args, 'cfg/endpoint-discovery']; + }, + 'cfgWithEndpointDiscoveryConfig' => function (): array { + $args = [ + 'endpoint_discovery' => new \Aws\EndpointDiscovery\Configuration ( + true, + 1000 + ), + ]; + + return [$args, 'cfg/endpoint-discovery']; + } + ]; + + foreach ($userAgentCases as $key => $case) { + if (is_callable($case)) { + yield $key => $case(); + } else { + yield $key => $case; + } + } + } + + /** + * Tests the user agent header values starts with the SDK/version string. + * Example: aws-sdk-php/3.x.x + * + * @return void + */ + public function testUserAgentValueStartsWithSdkVersionString() + { + $handler = UserAgentMiddleware::wrap([]); + $middleware = $handler(function ( + CommandInterface $command, + RequestInterface $request + ) { + $userAgent = $request->getHeaderLine('User-Agent'); + $pattern = "aws-sdk-php/" . Sdk::VERSION; + + $this->assertTrue( + substr($userAgent, 0, strlen($pattern)) === $pattern + ); + }); + $request = new Request('post', 'foo', [], 'buzz'); + $middleware(new Command('buzz'), $request); + } +} diff --git a/tests/WaiterTest.php b/tests/WaiterTest.php index 509ba921d1..e3aa991fa0 100644 --- a/tests/WaiterTest.php +++ b/tests/WaiterTest.php @@ -2,9 +2,11 @@ namespace Aws\Test; use Aws\Api\ApiProvider; +use Aws\AwsClientInterface; use Aws\CommandInterface; use Aws\DynamoDb\DynamoDbClient; use Aws\Exception\AwsException; +use Aws\MetricsBuilder; use Aws\Result; use Aws\S3\S3Client; use Aws\Waiter; @@ -25,6 +27,7 @@ class WaiterTest extends TestCase { use UsesServiceTrait; + use MetricsBuilderTestTrait; public function testErrorOnBadConfig() { @@ -425,23 +428,18 @@ public function testWaiterMatcherExpectNoError(): void 'Bucket' => 'fuzz', 'Key' => 'bazz' ]; - $waiterConfig = [ - 'delay' => 5, - 'operation' => 'headObject', - 'maxAttempts' => 20, - 'acceptors' => [ - [ - 'expected' => false, - 'matcher' => 'error', - 'state' => 'success' - ] + $acceptors = [ + [ + 'expected' => false, + 'matcher' => 'error', + 'state' => 'success' ] ]; - $waiter = new Waiter( - $client, - 'foo', + $waiter = $this->getTestWaiter( + $acceptors, + 'headObject', $commandArgs, - $waiterConfig + $client ); $waiter->promise() ->then(function (CommandInterface $_) { @@ -478,27 +476,87 @@ public function testWaiterMatcherExpectsAnyError(): void 'Bucket' => 'fuzz', 'Key' => 'bazz' ]; - $waiterConfig = [ - 'delay' => 5, - 'operation' => 'headObject', - 'maxAttempts' => 20, - 'acceptors' => [ - [ - 'expected' => true, - 'matcher' => 'error', - 'state' => 'success' - ] + $acceptors = [ + [ + 'expected' => true, + 'matcher' => 'error', + 'state' => 'success' ] ]; - $waiter = new Waiter( - $client, - 'foo', + $waiter = $this->getTestWaiter( + $acceptors, + 'headObject', $commandArgs, - $waiterConfig + $client ); $waiter->promise() ->then(function (CommandInterface $_) { $this->assertTrue(true); // Waiter succeeded })->wait(); } + + public function testAppendsMetricsCaptureMiddleware() + { + $client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $this->assertTrue( + in_array( + MetricsBuilder::WAITER, + $this->getMetricsAsArray($request) + ) + ); + + return new Response(); + } + ]); + $commandArgs = [ + 'Bucket' => 'foo' + ]; + $acceptors = [ + [ + 'expected' => 200, + 'matcher' => 'status', + 'state' => 'success' + ] + ]; + $waiter = $this->getTestWaiter( + $acceptors, + 'headBucket', + $commandArgs, + $client + ); + $waiter->promise()->wait(); + } + + /** + * Creates a test waiter. + * + * @param string $operation + * @param array $commandArgs + * @param AwsClientInterface $client + * + * @return Waiter + */ + private function getTestWaiter( + array $acceptors, + string $operation, + array $commandArgs, + AwsClientInterface $client + ): Waiter + { + $waiterConfig = [ + 'delay' => 5, + 'operation' => $operation, + 'maxAttempts' => 20, + 'acceptors' => $acceptors + ]; + + return new Waiter( + $client, + 'waiter-' . $operation, + $commandArgs, + $waiterConfig + ); + } } From f0830854bb23180e411d55224bd835b976a83005 Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Tue, 5 Nov 2024 08:58:44 -0800 Subject: [PATCH 2/5] 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 | 1240 ++++++++++++++++- 22 files changed, 1652 insertions(+), 191 deletions(-) create mode 100644 src/Credentials/CredentialSources.php diff --git a/src/AwsClient.php b/src/AwsClient.php index 2da7d6bef0..dcf5436a06 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -282,10 +282,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); } @@ -455,7 +451,7 @@ private function addSignatureMiddleware(array $args) } $resolver = static function ( - CommandInterface $c + CommandInterface $command ) use ( $api, $provider, @@ -467,17 +463,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'; @@ -492,20 +488,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( @@ -623,9 +620,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 ba9151edf1..d23a0ec276 100644 --- a/src/Sts/StsClient.php +++ b/src/Sts/StsClient.php @@ -72,7 +72,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'); @@ -97,7 +97,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 444dd5f827..fc899b943a 100644 --- a/tests/AwsClientTest.php +++ b/tests/AwsClientTest.php @@ -999,25 +999,6 @@ public function testQueryModeHeaderAdded(): void $client->TestOperation(); } - 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..000205061c 100644 --- a/tests/UserAgentMiddlewareTest.php +++ b/tests/UserAgentMiddlewareTest.php @@ -1,24 +1,99 @@ 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); + } + + 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 +138,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 +150,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 +189,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 +341,1145 @@ 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() + { + $profile = 'metric-test-profile'; + $configPath = $this->awsDir . '/credentials'; + putenv("AWS_PROFILE=$profile"); + putenv("HOME=" . $this->tempDir); + putenv("AWS_ACCESS_KEY_ID"); + putenv("AWS_SECRET_ACCESS_KEY"); + $profileContent = << '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(); + } } From 36006570fe8c058c12932da7fd99f5d4da305d03 Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Wed, 13 Nov 2024 17:24:52 -0800 Subject: [PATCH 3/5] chore: address PR feedback - Use static mapping instead of multiple if/else-ifs. - Remove the endpoint_id metric tracking since it was removed from the spec. - Make method's name to be verb based. - Remove the getMetricsBuilder from the CommandInterface. --- src/AwsClient.php | 4 +- src/Command.php | 3 +- src/CommandInterface.php | 7 --- src/Credentials/CredentialSources.php | 2 +- src/EndpointV2/EndpointV2Middleware.php | 4 -- src/MetricsBuilder.php | 84 ++++++++++--------------- src/UserAgentMiddleware.php | 48 +++++++------- tests/MetricsBuilderTest.php | 23 ------- tests/UserAgentMiddlewareTest.php | 35 ----------- 9 files changed, 60 insertions(+), 150 deletions(-) diff --git a/src/AwsClient.php b/src/AwsClient.php index dcf5436a06..11fe20023c 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -440,7 +440,6 @@ private function addSignatureMiddleware(array $args) $name = $this->config['signing_name']; $region = $this->config['signing_region']; $signingRegionSet = $this->signingRegionSet; - $handlerList = $this->getHandlerList(); if (isset($args['signature_version']) || isset($this->config['configured_signature_version']) @@ -459,8 +458,7 @@ private function addSignatureMiddleware(array $args) $region, $signatureVersion, $configuredSignatureVersion, - $signingRegionSet, - $handlerList + $signingRegionSet ) { if (!$configuredSignatureVersion) { if (!empty($command['@context']['signing_region'])) { diff --git a/src/Command.php b/src/Command.php index f6c3991355..949f7f4ed8 100644 --- a/src/Command.php +++ b/src/Command.php @@ -121,7 +121,8 @@ public function get($name) } /** - * @inheridoc + * Returns the metrics builder instance tied up to this command. + * * @internal * * @return MetricsBuilder diff --git a/src/CommandInterface.php b/src/CommandInterface.php index 853338039a..b35c75d37b 100644 --- a/src/CommandInterface.php +++ b/src/CommandInterface.php @@ -39,11 +39,4 @@ public function hasParam($name); * @return HandlerList */ public function getHandlerList(); - - /** - * Returns the metrics builder instance tied up to this command. - * - * @return MetricsBuilder - */ - public function getMetricsBuilder(); } diff --git a/src/Credentials/CredentialSources.php b/src/Credentials/CredentialSources.php index 6480b7c838..08dd678e0d 100644 --- a/src/Credentials/CredentialSources.php +++ b/src/Credentials/CredentialSources.php @@ -5,7 +5,7 @@ /** * @internal */ -class CredentialSources +final class CredentialSources { const STATIC = 'static'; const ENVIRONMENT = 'env'; diff --git a/src/EndpointV2/EndpointV2Middleware.php b/src/EndpointV2/EndpointV2Middleware.php index 0f141037c6..c271baae15 100644 --- a/src/EndpointV2/EndpointV2Middleware.php +++ b/src/EndpointV2/EndpointV2Middleware.php @@ -103,10 +103,6 @@ public function __invoke(CommandInterface $command) $command->getMetricsBuilder()->append(MetricsBuilder::RESOLVED_ACCOUNT_ID); } $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs); - $command->getMetricsBuilder()->identifyMetricByValueAndAppend( - 'account_id_endpoint', - $endpoint->getUrl() - ); if (!empty($authSchemes = $endpoint->getProperty('authSchemes'))) { $this->applyAuthScheme( $authSchemes, diff --git a/src/MetricsBuilder.php b/src/MetricsBuilder.php index a267088702..d27634f290 100644 --- a/src/MetricsBuilder.php +++ b/src/MetricsBuilder.php @@ -132,7 +132,6 @@ public function identifyMetricByValueAndAppend( static $appendMetricFns = [ 'signature' => 'appendSignatureMetric', 'request_compression' => 'appendRequestCompressionMetric', - 'account_id_endpoint' => 'appendAccountIdEndpoint', 'request_checksum' => 'appendRequestChecksumMetric', 'credentials' => 'appendCredentialsMetric' ]; @@ -171,22 +170,6 @@ private function appendRequestCompressionMetric(string $format): void } } - /** - * 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. * @@ -227,30 +210,34 @@ private function appendCredentialsMetric( 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); + static $credentialsMetricMapping = [ + CredentialSources::STATIC => + MetricsBuilder::CREDENTIALS_CODE, + CredentialSources::ENVIRONMENT => + MetricsBuilder::CREDENTIALS_ENV_VARS, + CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN => + MetricsBuilder::CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN, + CredentialSources::STS_ASSUME_ROLE => + MetricsBuilder::CREDENTIALS_STS_ASSUME_ROLE, + CredentialSources::STS_WEB_ID_TOKEN => + MetricsBuilder::CREDENTIALS_STS_ASSUME_ROLE_WEB_ID, + CredentialSources::PROFILE => + MetricsBuilder::CREDENTIALS_PROFILE, + CredentialSources::IMDS => + MetricsBuilder::CREDENTIALS_IMDS, + CredentialSources::ECS => + MetricsBuilder::CREDENTIALS_HTTP, + CredentialSources::PROFILE_STS_WEB_ID_TOKEN => + MetricsBuilder::CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN, + CredentialSources::PROCESS => + MetricsBuilder::CREDENTIALS_PROCESS, + CredentialSources::SSO => + MetricsBuilder::CREDENTIALS_SSO, + CredentialSources::SSO_LEGACY => + MetricsBuilder::CREDENTIALS_SSO_LEGACY, + ]; + if (isset($credentialsMetricMapping[$source])) { + $this->append($credentialsMetricMapping[$source]); } } @@ -269,25 +256,18 @@ private function appendCredentialsMetric( */ private function canMetricBeAppended(string $newMetric): bool { + if ($newMetric === "") { + return false; + } + if ($this->metricsSize + (strlen($newMetric) + strlen(self::$METRIC_SEPARATOR)) > self::$MAX_METRICS_SIZE ) { - @trigger_error( - "The metric `{$newMetric}` " - . "can not be added due to size constraints", - E_USER_WARNING - ); - return false; } if (isset($this->metrics[$newMetric])) { - @trigger_error( - 'The metric ' . $newMetric. ' is already appended!', - E_USER_WARNING - ); - return false; } diff --git a/src/UserAgentMiddleware.php b/src/UserAgentMiddleware.php index 1ec0da2dd2..066e133270 100644 --- a/src/UserAgentMiddleware.php +++ b/src/UserAgentMiddleware.php @@ -16,20 +16,20 @@ class UserAgentMiddleware { const AGENT_VERSION = 2.1; static $userAgentFnList = [ - 'sdkVersion', - 'userAgentVersion', - 'hhvmVersion', - 'osName', - 'langVersion', - 'execEnv', - 'endpointDiscovery', - 'appId', - 'metrics' + 'getSdkVersion', + 'getUserAgentVersion', + 'getHhvmVersion', + 'getOsName', + 'getLangVersion', + 'getExecEnv', + 'getEndpointDiscovery', + 'getAppId', + 'getMetrics' ]; static $metricsFnList = [ - 'endpointMetric', - 'accountIdModeMetric', - 'retryConfigMetric', + 'appendEndpointMetric', + 'appendAccountIdModeMetric', + 'appendRetryConfigMetric', ]; /** @var callable */ @@ -138,7 +138,7 @@ private function buildUserAgentValue(): array * * @return string */ - private function sdkVersion(): string + private function getSdkVersion(): string { return 'aws-sdk-php/' . Sdk::VERSION; } @@ -148,7 +148,7 @@ private function sdkVersion(): string * * @return string */ - private function userAgentVersion(): string + private function getUserAgentVersion(): string { return 'ua/' . self::AGENT_VERSION; } @@ -159,7 +159,7 @@ private function userAgentVersion(): string * * @return string */ - private function hhvmVersion(): string + private function getHhvmVersion(): string { if (defined('HHVM_VERSION')) { return 'HHVM/' . HHVM_VERSION; @@ -173,7 +173,7 @@ private function hhvmVersion(): string * * @return string */ - private function osName(): string + private function getOsName(): string { $disabledFunctions = explode(',', ini_get('disable_functions')); if (function_exists('php_uname') @@ -193,7 +193,7 @@ private function osName(): string * * @return string */ - private function langVersion(): string + private function getLangVersion(): string { return 'lang/php#' . phpversion(); } @@ -203,7 +203,7 @@ private function langVersion(): string * * @return string */ - private function execEnv(): string + private function getExecEnv(): string { if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { return $executionEnvironment; @@ -218,7 +218,7 @@ private function execEnv(): string * * @return string */ - private function endpointDiscovery(): string + private function getEndpointDiscovery(): string { $args = $this->args; if (isset($args['endpoint_discovery'])) { @@ -243,7 +243,7 @@ private function endpointDiscovery(): string * * @return string */ - private function appId(): string + private function getAppId(): string { if (empty($this->args['app_id'])) { return ""; @@ -257,7 +257,7 @@ private function appId(): string * * @return string */ - private function metrics(): string + private function getMetrics(): string { foreach (self::$metricsFnList as $fn) { $this->{$fn}(); @@ -275,7 +275,7 @@ private function metrics(): string * Appends the endpoint metric into the metrics builder, * just if a custom endpoint was provided at client construction. */ - private function endpointMetric(): void + private function appendEndpointMetric(): void { if (!empty($this->args['endpoint'])) { $this->metricsBuilder->append(MetricsBuilder::ENDPOINT_OVERRIDE); @@ -286,7 +286,7 @@ private function endpointMetric(): void * Appends the account id endpoint mode metric into the metrics builder, * based on the account id endpoint mode provide as client argument. */ - private function accountIdModeMetric(): void + private function appendAccountIdModeMetric(): void { $accountIdMode = $this->args['account_id_endpoint_mode'] ?? null; if ($accountIdMode === null) { @@ -306,7 +306,7 @@ private function accountIdModeMetric(): void * Appends the retry mode metric into the metrics builder, * based on the resolved retry config mode. */ - private function retryConfigMetric(): void + private function appendRetryConfigMetric(): void { $retries = $this->args['retries'] ?? null; if ($retries === null) { diff --git a/tests/MetricsBuilderTest.php b/tests/MetricsBuilderTest.php index 99c9e23cd4..eda1a7f9d9 100644 --- a/tests/MetricsBuilderTest.php +++ b/tests/MetricsBuilderTest.php @@ -61,29 +61,6 @@ static function ( $errno, $errstr ) { } } - public function testEmitMetricsSizeConstraintWarning() - { - try { - // Prevent deprecation warning for expectWarning - set_error_handler( - static function ( $errno, $errstr ) { - throw new \Exception( $errstr, $errno ); - }, - E_ALL - ); - $this->expectException(Exception::class); - $this->expectExceptionMessage( - "The metric `A` can not be added due to size constraints" - ); - $metricsBuilder = new MetricsBuilder(); - $firstMetric = str_repeat("*", 1024); - $metricsBuilder->append($firstMetric); - $metricsBuilder->append("A"); - } finally { - restore_error_handler(); - } - } - public function testGetMetricsBuilderFromCommand() { $command = new Command('TestCommand', [], new HandlerList()); diff --git a/tests/UserAgentMiddlewareTest.php b/tests/UserAgentMiddlewareTest.php index 000205061c..2cc14c4396 100644 --- a/tests/UserAgentMiddlewareTest.php +++ b/tests/UserAgentMiddlewareTest.php @@ -650,41 +650,6 @@ public function testUserAgentCaptureGzipRequestCompressionMetric() ]); } - /** - * 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. * From b8ab573c37f8ee26012d11af78aa76516661f6cf Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Fri, 15 Nov 2024 07:12:53 -0800 Subject: [PATCH 4/5] chore: add credentials->source tests - Add tests cases to cover credentials sets the correct source when resolved. - Fix the sso, sso_legacy, and process metrics. They should be all a profile based metric. --- src/Credentials/CredentialProvider.php | 6 +- src/Credentials/CredentialSources.php | 6 +- src/Credentials/Credentials.php | 2 +- src/MetricsBuilder.php | 18 +- tests/Credentials/CredentialProviderTest.php | 649 +++++++++++++++++++ tests/UserAgentMiddlewareTest.php | 8 +- 6 files changed, 670 insertions(+), 19 deletions(-) diff --git a/src/Credentials/CredentialProvider.php b/src/Credentials/CredentialProvider.php index 65c0dccf10..ea438ea2b2 100644 --- a/src/Credentials/CredentialProvider.php +++ b/src/Credentials/CredentialProvider.php @@ -646,7 +646,7 @@ public static function process($profile = null, $filename = null) $processData['SessionToken'], $expires, $accountId, - CredentialSources::PROCESS + CredentialSources::PROFILE_PROCESS ) ); }; @@ -927,7 +927,7 @@ private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $ssoCredentials['sessionToken'], $expiration, $ssoProfile['sso_account_id'], - CredentialSources::SSO + CredentialSources::PROFILE_SSO ) ); } @@ -988,7 +988,7 @@ private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $fil $ssoCredentials['sessionToken'], $expiration, $ssoProfile['sso_account_id'], - CredentialSources::SSO_LEGACY + CredentialSources::PROFILE_SSO_LEGACY ) ); } diff --git a/src/Credentials/CredentialSources.php b/src/Credentials/CredentialSources.php index 08dd678e0d..829aa919c1 100644 --- a/src/Credentials/CredentialSources.php +++ b/src/Credentials/CredentialSources.php @@ -16,7 +16,7 @@ final class CredentialSources const PROFILE = 'profile'; const IMDS = 'instance_profile_provider'; const ECS = 'ecs'; - const SSO = 'sso'; - const SSO_LEGACY = 'sso_legacy'; - const PROCESS = 'process'; + const PROFILE_SSO = 'profile_sso'; + const PROFILE_SSO_LEGACY = 'profile_sso_legacy'; + const PROFILE_PROCESS = 'profile_process'; } diff --git a/src/Credentials/Credentials.php b/src/Credentials/Credentials.php index 70066a745f..766de78cd6 100644 --- a/src/Credentials/Credentials.php +++ b/src/Credentials/Credentials.php @@ -41,7 +41,7 @@ public function __construct( $this->token = $token; $this->expires = $expires; $this->accountId = $accountId; - $this->source = $source; + $this->source = $source ?? CredentialSources::STATIC; } public static function __set_state(array $state) diff --git a/src/MetricsBuilder.php b/src/MetricsBuilder.php index d27634f290..38184450f0 100644 --- a/src/MetricsBuilder.php +++ b/src/MetricsBuilder.php @@ -43,9 +43,9 @@ final class MetricsBuilder 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"; + const CREDENTIALS_PROFILE_PROCESS = "v"; + const CREDENTIALS_PROFILE_SSO = "r"; + const CREDENTIALS_PROFILE_SSO_LEGACY = "t"; /** @var int */ private static $MAX_METRICS_SIZE = 1024; // 1KB or 1024 B @@ -229,12 +229,12 @@ private function appendCredentialsMetric( MetricsBuilder::CREDENTIALS_HTTP, CredentialSources::PROFILE_STS_WEB_ID_TOKEN => MetricsBuilder::CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN, - CredentialSources::PROCESS => - MetricsBuilder::CREDENTIALS_PROCESS, - CredentialSources::SSO => - MetricsBuilder::CREDENTIALS_SSO, - CredentialSources::SSO_LEGACY => - MetricsBuilder::CREDENTIALS_SSO_LEGACY, + CredentialSources::PROFILE_PROCESS => + MetricsBuilder::CREDENTIALS_PROFILE_PROCESS, + CredentialSources::PROFILE_SSO => + MetricsBuilder::CREDENTIALS_PROFILE_SSO, + CredentialSources::PROFILE_SSO_LEGACY => + MetricsBuilder::CREDENTIALS_PROFILE_SSO_LEGACY, ]; if (isset($credentialsMetricMapping[$source])) { $this->append($credentialsMetricMapping[$source]); diff --git a/tests/Credentials/CredentialProviderTest.php b/tests/Credentials/CredentialProviderTest.php index 73701f1b53..4ce4a2990f 100644 --- a/tests/Credentials/CredentialProviderTest.php +++ b/tests/Credentials/CredentialProviderTest.php @@ -2,6 +2,7 @@ namespace Aws\Test\Credentials; use Aws\Api\DateTimeResult; +use Aws\Credentials\AssumeRoleWithWebIdentityCredentialProvider; use Aws\Credentials\CredentialProvider; use Aws\Credentials\Credentials; use Aws\Credentials\CredentialSources; @@ -10,10 +11,16 @@ use Aws\History; use Aws\LruArrayCache; use Aws\Result; +use Aws\SSO\SSOClient; +use Aws\Sts\StsClient; use Aws\Token\SsoTokenProvider; use GuzzleHttp\Promise; use Aws\Test\UsesServiceTrait; +use GuzzleHttp\Promise\Create; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Utils; use Yoast\PHPUnitPolyfills\TestCases\TestCase; +use function Aws\dir_iterator; /** @@ -2066,4 +2073,646 @@ public function shouldUseEcsProvider() ['', '', '', '', false] ]; } + + /** + * Test credentials defaults source to `static`. + * + * @return void + */ + public function testCredentialsSourceFromStatic() + { + $credentials = new Credentials('foo', 'foo'); + + $this->assertEquals( + CredentialSources::STATIC, + $credentials->getSource() + ); + } + + /** + * Test credentials from environment, sets source to `env`. + * + * @return void + */ + public function testCredentialsSourceFromEnv() + { + $currentEnv = [ + 'AWS_ACCESS_KEY_ID' => getenv('AWS_ACCESS_KEY_ID'), + 'AWS_SECRET_ACCESS_KEY' => getenv('AWS_SECRET_ACCESS_KEY') + ]; + putenv('AWS_ACCESS_KEY_ID=foo'); + putenv('AWS_SECRET_ACCESS_KEY=bazz'); + try { + $credentialsProvider = CredentialProvider::env(); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::ENVIRONMENT, + $credentials->getSource() + ); + } finally { + foreach ($currentEnv as $key => $value) { + if ($value !== false) { + putenv("$key=$value"); + } else { + putenv("$key"); + } + } + } + } + + /** + * Test credentials from sts web id token, sets source to `sts_web_id_token`. + * + * @return void + */ + public function testCredentialsSourceFromStsWebIdToken() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $tokenPath = $awsDir . '/my-token.jwt'; + file_put_contents($tokenPath, 'token'); + $roleArn = 'arn:aws:iam::123456789012:role/role_name'; + $result = [ + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'bar', + 'SessionToken' => 'baz', + 'Expiration' => time() + 1000 + ], + 'AssumedRoleUser' => [ + 'AssumedRoleId' => 'test_user_621903f1f21f5.01530789', + 'Arn' => $roleArn + ] + ]; + try { + $stsClient = new StsClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + return Create::promiseFor(new Result($result)); + } + ]); + $credentialsProvider = new AssumeRoleWithWebIdentityCredentialProvider([ + 'RoleArn' => $roleArn, + 'WebIdentityTokenFile' => $tokenPath, + 'client' => $stsClient + ]); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::STS_WEB_ID_TOKEN, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + } + } + + /** + * Test credentials from sts web id token defined by env, sets source to + * `env_sts_web_id_token`. + * + * @return void + */ + public function testCredentialsSourceFromEnvStsWebIdToken() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $tokenPath = $awsDir . '/my-token.jwt'; + file_put_contents($tokenPath, 'token'); + $roleArn = 'arn:aws:iam::123456789012:role/role_name'; + // Set temporary env values + $currentEnv = [ + 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 + ) + ]; + 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' => time() + 1000 + ], + 'AssumedRoleUser' => [ + 'AssumedRoleId' => 'test_user_621903f1f21f5.01530789', + 'Arn' => $roleArn + ] + ]; + try { + $stsClient = new StsClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + return Create::promiseFor(new Result($result)); + } + ]); + $credentialsProvider = + CredentialProvider::assumeRoleWithWebIdentityCredentialProvider([ + 'stsClient' => $stsClient + ]); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + foreach ($currentEnv as $key => $value) { + if ($value !== false) { + putenv("$key=$value"); + } else { + putenv("$key"); + } + } + } + } + + /** + * Test credentials from sts web id token defined by profile, sets source to + * `profile_sts_web_id_token`. + * + * @return void + */ + public function testCredentialsSourceFromProfileStsWebIdToken() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $tokenPath = $awsDir . '/my-token.jwt'; + file_put_contents($tokenPath, 'token'); + $roleArn = 'arn:aws:iam::123456789012:role/role_name'; + $profile = "test-profile"; + $configPath = $awsDir . '/my-config'; + $configData = << [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'bar', + 'SessionToken' => 'baz', + 'Expiration' => time() + 1000 + ], + 'AssumedRoleUser' => [ + 'AssumedRoleId' => 'test_user_621903f1f21f5.01530789', + 'Arn' => $roleArn + ] + ]; + try { + $stsClient = new StsClient([ + 'region' => 'us-east-1', + 'credentials' => false, + 'handler' => function ($command, $request) use ($result) { + return Create::promiseFor(new Result($result)); + } + ]); + $credentialsProvider = + CredentialProvider::assumeRoleWithWebIdentityCredentialProvider([ + 'stsClient' => $stsClient, + 'filename' => $configPath + ]); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::PROFILE_STS_WEB_ID_TOKEN, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + putenv(CredentialProvider::ENV_PROFILE); + } + } + + /** + * Test credentials from sts assume role, sets source to + * `sts_assume_role`. + * + * @return void + */ + public function testCredentialsSourceFromStsAssumeRole() + { + $stsClient = new StsClient([ + 'region' => 'us-east-2', + 'handler' => function ($command, $request) { + return Create::promiseFor( + new Result([ + 'Credentials' => [ + 'AccessKeyId' => 'foo', + 'SecretAccessKey' => 'foo' + ] + ]) + ); + } + ]); + $credentialsProvider = CredentialProvider::assumeRole([ + 'assume_role_params' => [ + 'RoleArn' => 'arn:aws:iam::account-id:role/role-name', + 'RoleSessionName' => 'foo_session' + ], + 'client' => $stsClient + ]); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::STS_ASSUME_ROLE, + $credentials->getSource() + ); + } + + /** + * Test credentials sourced from a profile, sets source to + * `profile`. + * + * @return void + */ + public function testCredentialsSourceFromProfile() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $profile = 'test-profile'; + $configPath = $awsDir . '/credentials'; + $configData = << getenv('AWS_ACCESS_KEY_ID'), + 'AWS_SECRET_ACCESS_KEY' => getenv('AWS_SECRET_ACCESS_KEY') + ]; + putenv("AWS_ACCESS_KEY_ID"); + putenv("AWS_SECRET_ACCESS_KEY"); + try { + $credentialsProvider = CredentialProvider::ini( + $profile, + $configPath + ); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::PROFILE, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + foreach ($currentEnv as $key => $value) { + if ($value !== false) { + putenv("$key=$value"); + } else { + putenv("$key"); + } + } + } + } + + /** + * Test credentials from IMDS, sets source to + * `instance_profile_provider`. + * + * @return void + */ + public function testCredentialsSourceFromIMDS() + { + $imdsHandler = function ($request) { + $path = $request->getUri()->getPath(); + if ($path === '/latest/api/token') { + return Create::promiseFor( + new Response(200, [], Utils::streamFor('')) + ); + } elseif ($path === '/latest/meta-data/iam/security-credentials/') { + return Create::promiseFor( + new Response(200, [], Utils::streamFor('testProfile')) + ); + } elseif ($path === '/latest/meta-data/iam/security-credentials/testProfile') { + $expiration = time() + 1000; + return Create::promiseFor( + new Response( + 200, + [], + Utils::streamFor( + << $imdsHandler, + ]); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::IMDS, + $credentials->getSource() + ); + } + + /** + * Test credentials from ECS, sets source to + * `ecs`. + * + * @return void + */ + public function testCredentialsSourceFromECS() + { + $ecsHandler = function ($request) { + $expiration = time() + 1000; + return Create::promiseFor( + new Response( + 200, + [], + << $ecsHandler, + ]); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::ECS, + $credentials->getSource() + ); + } + + /** + * Test credentials sourced from process, sets source to + * `profile_process`. + * + * @return void + */ + public function testCredentialsSourceFromProcess() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $profile = 'test-profile'; + $configData = <<wait(); + + $this->assertEquals( + CredentialSources::PROFILE_PROCESS, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + } + } + + /** + * Test credentials sourced from sso, sets source to + * `profile_sso`. + * + * @return void + */ + public function testCredentialsSourceFromSso() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $expiration = time() + 1000; + $ini = << [ + '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)); + } + ]); + try { + $credentialsProvider = CredentialProvider::sso( + 'default', + $configPath, + [ + 'ssoClient' => $ssoClient + ] + ); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::PROFILE_SSO, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + } + } + + /** + * Test credentials sourced from sso legacy, sets source to + * `profile_sso_legacy`. + * + * @return void + */ + public function testCredentialsSourceFromSsoLegacy() + { + $tempHomeDir = sys_get_temp_dir() . "/test_credentials_source"; + $awsDir = $tempHomeDir . "/.aws"; + if (!is_dir($awsDir)) { + mkdir($awsDir, 0777, true); + } + $expiration = time() + 1000; + $ini = << [ + '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)); + } + ]); + try { + $credentialsProvider = CredentialProvider::sso( + 'default', + $configPath, + [ + 'ssoClient' => $ssoClient + ] + ); + $credentials = $credentialsProvider()->wait(); + + $this->assertEquals( + CredentialSources::PROFILE_SSO_LEGACY, + $credentials->getSource() + ); + } finally { + $this->cleanUpDir($tempHomeDir); + } + } + + /** + * 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); + } } diff --git a/tests/UserAgentMiddlewareTest.php b/tests/UserAgentMiddlewareTest.php index 2cc14c4396..4362d49fb4 100644 --- a/tests/UserAgentMiddlewareTest.php +++ b/tests/UserAgentMiddlewareTest.php @@ -1272,7 +1272,8 @@ public function testUserAgentCaptureCredentialsProcessMetric() $metrics = $this->getMetricsAsArray($request); $this->assertTrue( - in_array(MetricsBuilder::CREDENTIALS_PROCESS, $metrics) + in_array(MetricsBuilder::CREDENTIALS_PROFILE_PROCESS, + $metrics) ); return new Response( @@ -1357,7 +1358,7 @@ public function testUserAgentCaptureCredentialsSSOMetric() $metrics = $this->getMetricsAsArray($request); $this->assertTrue( - in_array(MetricsBuilder::CREDENTIALS_SSO, $metrics) + in_array(MetricsBuilder::CREDENTIALS_PROFILE_SSO, $metrics) ); return new Response( @@ -1437,7 +1438,8 @@ public function testUserAgentCaptureCredentialsSSOLegacyMetric() $metrics = $this->getMetricsAsArray($request); $this->assertTrue( - in_array(MetricsBuilder::CREDENTIALS_SSO_LEGACY, $metrics) + in_array(MetricsBuilder::CREDENTIALS_PROFILE_SSO_LEGACY, + $metrics) ); return new Response( From eb1aa901c317a9a22c1acb1bb7d8b3891b854781 Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Thu, 21 Nov 2024 10:36:34 -0800 Subject: [PATCH 5/5] chore: add missing blank lines --- src/MetricsBuilder.php | 3 +++ src/UserAgentMiddleware.php | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/MetricsBuilder.php b/src/MetricsBuilder.php index 38184450f0..506b2c8020 100644 --- a/src/MetricsBuilder.php +++ b/src/MetricsBuilder.php @@ -49,10 +49,13 @@ final class MetricsBuilder /** @var int */ private static $MAX_METRICS_SIZE = 1024; // 1KB or 1024 B + /** @var string */ private static $METRIC_SEPARATOR = ","; + /** @var array $metrics */ private $metrics; + /** @var int $metricsSize */ private $metricsSize; diff --git a/src/UserAgentMiddleware.php b/src/UserAgentMiddleware.php index 066e133270..dfaddcedec 100644 --- a/src/UserAgentMiddleware.php +++ b/src/UserAgentMiddleware.php @@ -15,6 +15,7 @@ class UserAgentMiddleware { const AGENT_VERSION = 2.1; + static $userAgentFnList = [ 'getSdkVersion', 'getUserAgentVersion', @@ -26,6 +27,7 @@ class UserAgentMiddleware 'getAppId', 'getMetrics' ]; + static $metricsFnList = [ 'appendEndpointMetric', 'appendAccountIdModeMetric', @@ -34,8 +36,10 @@ class UserAgentMiddleware /** @var callable */ private $nextHandler; + /** @var array */ private $args; + /** @var MetricsBuilder */ private $metricsBuilder; @@ -68,6 +72,7 @@ public function __construct(callable $nextHandler, array $args=[]) /** * When invoked, its injects the user agent header into the * request headers. + * * @param CommandInterface $command * @param RequestInterface $request *