diff --git a/.changes/nextrelease/multi-auth.json b/.changes/nextrelease/multi-auth.json new file mode 100644 index 0000000000..1b7c3070a8 --- /dev/null +++ b/.changes/nextrelease/multi-auth.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "Auth", + "description": "Adds support for the `auth` service trait. This allows for auth scheme selection at both the service and operation level." + } +] diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 679ea32e74..a614ecc255 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -78,17 +78,21 @@ jobs: for encoding in encodings: try: with open(file_path, 'r', encoding=encoding) as f: - sourceCode = f.read() + source_code = f.read() break except UnicodeDecodeError: continue - pattern = r'(function\s+\w+\([^)]*\))\s*:\s*?\??\w+\s*\n\s*\{' - match = re.search(pattern, sourceCode) + pattern = r'(function\s+\w+\([^)]*\))\s*:\s*?\??\w+(\s*\n\s*\{|;)' + match = re.search(pattern, source_code) if match: - sourceCode = re.sub(pattern, r'\1 {', sourceCode) + def replace_function(match): + return match.group(1) + " {\n" if '{' in match.group(2) else match.group(1) + ';' + + new_source_code = re.sub(pattern, replace_function, source_code) + with open(file_path, 'w') as f: - f.write(sourceCode) + f.write(new_source_code) except FileNotFoundError: print('php file not found : ', file_path) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ec6f5ddae3..d32051d2b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,10 @@ - + src diff --git a/src/Auth/AuthSchemeResolver.php b/src/Auth/AuthSchemeResolver.php new file mode 100644 index 0000000000..fa66a878f8 --- /dev/null +++ b/src/Auth/AuthSchemeResolver.php @@ -0,0 +1,153 @@ + 'v4', + 'aws.auth#sigv4a' => 'v4a', + 'smithy.api#httpBearerAuth' => 'bearer', + 'smithy.auth#noAuth' => 'anonymous' + ]; + + /** + * @var array Mapping of auth schemes to signature versions used in + * resolving a signature version. + */ + private $authSchemeMap; + private $tokenProvider; + private $credentialProvider; + + + public function __construct( + callable $credentialProvider, + callable $tokenProvider = null, + array $authSchemeMap = [] + ){ + $this->credentialProvider = $credentialProvider; + $this->tokenProvider = $tokenProvider; + $this->authSchemeMap = empty($authSchemeMap) + ? self::$defaultAuthSchemeMap + : $authSchemeMap; + } + + /** + * Accepts a priority-ordered list of auth schemes and an Identity + * and selects the first compatible auth schemes, returning a normalized + * signature version. For example, based on the default auth scheme mapping, + * if `aws.auth#sigv4` is selected, `v4` will be returned. + * + * @param array $authSchemes + * @param $identity + * + * @return string + * @throws UnresolvedAuthSchemeException + */ + public function selectAuthScheme( + array $authSchemes, + array $args = [] + ): string + { + $failureReasons = []; + + foreach($authSchemes as $authScheme) { + $normalizedAuthScheme = isset($this->authSchemeMap[$authScheme]) + ? $this->authSchemeMap[$authScheme] + : $authScheme; + + if ($this->isCompatibleAuthScheme($normalizedAuthScheme)) { + if ($normalizedAuthScheme === 'v4' && !empty($args['unsigned_payload'])) { + return $normalizedAuthScheme . self::UNSIGNED_BODY; + } + + return $normalizedAuthScheme; + } else { + $failureReasons[] = $this->getIncompatibilityMessage($authScheme); + } + } + + throw new UnresolvedAuthSchemeException( + 'Could not resolve an authentication scheme: ' + . implode('; ', $failureReasons) + ); + } + + /** + * Determines compatibility based on either Identity or the availability + * of the CRT extension. + * + * @param $authScheme + * + * @return bool + */ + private function isCompatibleAuthScheme($authScheme): bool + { + switch ($authScheme) { + case 'v4': + case 'anonymous': + return $this->hasAwsCredentialIdentity(); + case 'v4a': + return extension_loaded('awscrt') && $this->hasAwsCredentialIdentity(); + case 'bearer': + return $this->hasBearerTokenIdentity(); + default: + return false; + } + } + + /** + * Provides incompatibility messages in the event an incompatible auth scheme + * is encountered. + * + * @param $authScheme + * + * @return string + */ + private function getIncompatibilityMessage($authScheme): string + { + switch ($authScheme) { + case 'v4a': + return 'The aws-crt-php extension must be installed to use Signature V4A'; + case 'bearer': + return 'Bearer token credentials must be provided to use Bearer authentication'; + default: + return "The service does not support `{$authScheme}` authentication."; + } + } + + /** + * @return bool + */ + private function hasAwsCredentialIdentity(): bool + { + $fn = $this->credentialProvider; + return $fn()->wait() instanceof AwsCredentialIdentity; + } + + /** + * @return bool + */ + private function hasBearerTokenIdentity(): bool + { + if ($this->tokenProvider) { + $fn = $this->tokenProvider; + return $fn()->wait() instanceof BearerTokenIdentity; + } + return false; + } +} diff --git a/src/Auth/AuthSchemeResolverInterface.php b/src/Auth/AuthSchemeResolverInterface.php new file mode 100644 index 0000000000..54a59aa68e --- /dev/null +++ b/src/Auth/AuthSchemeResolverInterface.php @@ -0,0 +1,24 @@ +nextHandler = $nextHandler; + $this->authResolver = $authResolver; + $this->api = $api; + } + + /** + * @param CommandInterface $command + * + * @return Promise + */ + public function __invoke(CommandInterface $command) + { + $nextHandler = $this->nextHandler; + $serviceAuth = $this->api->getMetadata('auth') ?: []; + $operation = $this->api->getOperation($command->getName()); + $operationAuth = isset($operation['auth']) ? $operation['auth'] : []; + $unsignedPayload = isset($operation['unsignedpayload']) + ? $operation['unsignedpayload'] + : false; + $resolvableAuth = $operationAuth ?: $serviceAuth; + + if (!empty($resolvableAuth)) { + if (isset($command['@context']['auth_scheme_resolver']) + && $command['@context']['auth_scheme_resolver'] instanceof AuthSchemeResolverInterface + ){ + $resolver = $command['@context']['auth_scheme_resolver']; + } else { + $resolver = $this->authResolver; + } + + $selectedAuthScheme = $resolver->selectAuthScheme( + $resolvableAuth, + ['unsigned_payload' => $unsignedPayload] + ); + + if (!empty($selectedAuthScheme)) { + $command['@context']['signature_version'] = $selectedAuthScheme; + } + } + + return $nextHandler($command); + } +} diff --git a/src/AwsClient.php b/src/AwsClient.php index 33e270017d..3a2af99d43 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -4,6 +4,8 @@ use Aws\Api\ApiProvider; use Aws\Api\DocModel; use Aws\Api\Service; +use Aws\Auth\AuthSelectionMiddleware; +use Aws\Auth\AuthSchemeResolverInterface; use Aws\EndpointDiscovery\EndpointDiscoveryMiddleware; use Aws\EndpointV2\EndpointProviderV2; use Aws\EndpointV2\EndpointV2Middleware; @@ -36,6 +38,9 @@ class AwsClient implements AwsClientInterface /** @var callable */ private $signatureProvider; + /** @var AuthSchemeResolverInterface */ + private $authSchemeResolver; + /** @var callable */ private $credentialProvider; @@ -223,6 +228,7 @@ public function __construct(array $args) $config = $resolver->resolve($args, $this->handlerList); $this->api = $config['api']; $this->signatureProvider = $config['signature_provider']; + $this->authSchemeResolver = $config['auth_scheme_resolver']; $this->endpoint = new Uri($config['endpoint']); $this->credentialProvider = $config['credentials']; $this->tokenProvider = $config['token']; @@ -244,6 +250,7 @@ public function __construct(array $args) if ($this->isUseEndpointV2()) { $this->addEndpointV2Middleware(); } + $this->addAuthSelectionMiddleware(); if (!is_null($this->api->getMetadata('awsQueryCompatible'))) { $this->addQueryCompatibleInputMiddleware($this->api); @@ -407,45 +414,34 @@ private function addSignatureMiddleware() { $api = $this->getApi(); $provider = $this->signatureProvider; - $version = $this->config['signature_version']; + $signatureVersion = $this->config['signature_version']; $name = $this->config['signing_name']; $region = $this->config['signing_region']; $resolver = static function ( CommandInterface $c - ) use ($api, $provider, $name, $region, $version) { + ) use ($api, $provider, $name, $region, $signatureVersion) { if (!empty($c['@context']['signing_region'])) { $region = $c['@context']['signing_region']; } if (!empty($c['@context']['signing_service'])) { $name = $c['@context']['signing_service']; } + if (!empty($c['@context']['signature_version'])) { + $signatureVersion = $c['@context']['signature_version']; + } $authType = $api->getOperation($c->getName())['authtype']; - switch ($authType){ + switch ($authType) { case 'none': - $version = 'anonymous'; + $signatureVersion = 'anonymous'; break; case 'v4-unsigned-body': - $version = 'v4-unsigned-body'; + $signatureVersion = 'v4-unsigned-body'; break; - case 'bearer': - $version = 'bearer'; - break; - } - if (isset($c['@context']['signature_version'])) { - if ($c['@context']['signature_version'] == 'v4a') { - $version = 'v4a'; - } } - if (!empty($endpointAuthSchemes = $c->getAuthSchemes())) { - $version = $endpointAuthSchemes['version']; - $name = isset($endpointAuthSchemes['name']) ? - $endpointAuthSchemes['name'] : $name; - $region = isset($endpointAuthSchemes['region']) ? - $endpointAuthSchemes['region'] : $region; - } - return SignatureProvider::resolve($provider, $version, $name, $region); + + return SignatureProvider::resolve($provider, $signatureVersion, $name, $region); }; $this->handlerList->appendSign( Middleware::signer($this->credentialProvider, @@ -519,6 +515,19 @@ private function addRecursionDetection() ); } + private function addAuthSelectionMiddleware() + { + $list = $this->getHandlerList(); + + $list->prependBuild( + AuthSelectionMiddleware::wrap( + $this->authSchemeResolver, + $this->getApi() + ), + 'auth-selection' + ); + } + private function addEndpointV2Middleware() { $list = $this->getHandlerList(); @@ -548,7 +557,7 @@ private function setClientContextParams($args) if (!empty($paramDefinitions = $api->getClientContextParams())) { foreach($paramDefinitions as $paramName => $paramValue) { if (isset($args[$paramName])) { - $resolvedParams[$paramName] = $args[$paramName]; + $result[$paramName] = $args[$paramName]; } } } diff --git a/src/ClientResolver.php b/src/ClientResolver.php index fd9427dc26..ce486e269f 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -4,6 +4,9 @@ use Aws\Api\ApiProvider; use Aws\Api\Service; use Aws\Api\Validator; +use Aws\Auth\AuthResolver; +use Aws\Auth\AuthSchemeResolver; +use Aws\Auth\AuthSchemeResolverInterface; use Aws\ClientSideMonitoring\ApiCallAttemptMonitoringMiddleware; use Aws\ClientSideMonitoring\ApiCallMonitoringMiddleware; use Aws\ClientSideMonitoring\Configuration; @@ -196,6 +199,12 @@ class ClientResolver 'fn' => [__CLASS__, '_apply_token'], 'default' => [__CLASS__, '_default_token_provider'], ], + 'auth_scheme_resolver' => [ + 'type' => 'value', + 'valid' => [AuthSchemeResolverInterface::class], + 'doc' => 'An instance of Aws\Auth\AuthSchemeResolverInterface which selects a modeled auth scheme and returns a signature version', + 'default' => [__CLASS__, '_default_auth_scheme_resolver'], + ], 'endpoint_discovery' => [ 'type' => 'value', 'valid' => [ConfigurationInterface::class, CacheInterface::class, 'array', 'callable'], @@ -1075,6 +1084,11 @@ public static function _default_signature_provider() return SignatureProvider::defaultProvider(); } + public static function _default_auth_scheme_resolver(array $args) + { + return new AuthSchemeResolver($args['credentials'], $args['token']); + } + public static function _default_signature_version(array &$args) { if (isset($args['config']['signature_version'])) { diff --git a/src/Command.php b/src/Command.php index 389a2941cd..ea7d84a3c1 100644 --- a/src/Command.php +++ b/src/Command.php @@ -60,27 +60,23 @@ public function getHandlerList() return $this->handlerList; } - /** - * For overriding auth schemes on a per endpoint basis when using - * EndpointV2 provider. Intended for internal use only. - * - * @param array $authSchemes - * - * @internal - */ - public function setAuthSchemes(array $authSchemes) - { - $this->authSchemes = $authSchemes; - } - /** * Get auth schemes added to command as required * for endpoint resolution * * @returns array + * + * @deprecated In favor of using the @context property bag. + * Auth schemes are now accessible via the `signature_version` key + * in a Command's context, if applicable. */ public function getAuthSchemes() { + trigger_error(__METHOD__ . ' is deprecated. Auth schemes ' + . 'resolved using the service `auth` trait or via endpoint resolution ' + . 'can now be found in the command `@context` property.`' + , E_USER_DEPRECATED); + return $this->authSchemes ?: []; } diff --git a/src/Credentials/Credentials.php b/src/Credentials/Credentials.php index 49af46310f..de8964e8c7 100644 --- a/src/Credentials/Credentials.php +++ b/src/Credentials/Credentials.php @@ -1,11 +1,15 @@ endpointProvider->getRuleset()->getParameters(); $endpointCommandArgs = $this->filterEndpointCommandArgs( @@ -144,7 +144,7 @@ private function resolveArgs(array $commandArgs, Operation $operation) : array private function filterEndpointCommandArgs( array $rulesetParams, array $commandArgs - ) : array + ): array { $endpointMiddlewareOpts = [ '@use_dual_stack_endpoint' => 'UseDualStack', @@ -181,7 +181,7 @@ private function filterEndpointCommandArgs( * * @return array */ - private function bindStaticContextParams($staticContextParams) : array + private function bindStaticContextParams($staticContextParams): array { $scopedParams = []; @@ -204,7 +204,7 @@ private function bindStaticContextParams($staticContextParams) : array private function bindContextParams( array $commandArgs, array $contextParams - ) : array + ): array { $scopedParams = []; @@ -228,10 +228,21 @@ private function bindContextParams( private function applyAuthScheme( array $authSchemes, CommandInterface $command - ) : void + ): void { $authScheme = $this->resolveAuthScheme($authSchemes); - $command->setAuthSchemes($authScheme); + + $command['@context']['signature_version'] = $authScheme['version']; + + if (isset($authScheme['name'])) { + $command['@context']['signing_service'] = $authScheme['name']; + } + + if (isset($authScheme['region'])) { + $command['@context']['signing_region'] = $authScheme['region']; + } elseif (isset($authScheme['signingRegionSet'])) { + $command['@context']['signing_region_set'] = $authScheme['signingRegionSet']; + } } /** @@ -242,7 +253,7 @@ private function applyAuthScheme( * * @return array */ - private function resolveAuthScheme(array $authSchemes) : array + private function resolveAuthScheme(array $authSchemes): array { $invalidAuthSchemes = []; @@ -275,7 +286,7 @@ private function resolveAuthScheme(array $authSchemes) : array * @param array $authScheme * @return array */ - private function normalizeAuthScheme(array $authScheme) : array + private function normalizeAuthScheme(array $authScheme): array { /* sigv4a will contain a regionSet property. which is guaranteed to be `*` diff --git a/src/Identity/AwsCredentialIdentity.php b/src/Identity/AwsCredentialIdentity.php new file mode 100644 index 0000000000..77a0320331 --- /dev/null +++ b/src/Identity/AwsCredentialIdentity.php @@ -0,0 +1,13 @@ +cache = new LruArrayCache(100); diff --git a/src/QueryCompatibleInputMiddleware.php b/src/QueryCompatibleInputMiddleware.php index 4b5cab88dc..913a57e2e6 100644 --- a/src/QueryCompatibleInputMiddleware.php +++ b/src/QueryCompatibleInputMiddleware.php @@ -102,7 +102,7 @@ private function processStructure( array $input, StructureShape $shape, array $path - ) : void + ): void { foreach ($input as $param => $value) { if ($shape->hasMember($param)) { @@ -123,7 +123,7 @@ private function processList( array $input, ListShape $shape, array $path - ) : void + ): void { foreach ($input as $param => $value) { $memberPath = array_merge($path, [$param]); @@ -138,7 +138,7 @@ private function processList( * * @return void */ - private function processMap(array $input, MapShape $shape, array $path) : void + private function processMap(array $input, MapShape $shape, array $path): void { foreach ($input as $param => $value) { $memberPath = array_merge($path, [$param]); @@ -153,7 +153,7 @@ private function processMap(array $input, MapShape $shape, array $path) : void * * @return void */ - private function processScalar($input, Shape $shape, array $path) : void + private function processScalar($input, Shape $shape, array $path): void { $expectedType = $shape->getType(); @@ -177,7 +177,7 @@ private function processScalar($input, Shape $shape, array $path) : void * * @return void */ - private function changeValueAtPath(array $path, $newValue) : void + private function changeValueAtPath(array $path, $newValue): void { $commandRef = &$this->command; @@ -196,7 +196,7 @@ private function changeValueAtPath(array $path, $newValue) : void * * @return bool */ - private function isModeledType($value, $type) : bool + private function isModeledType($value, $type): bool { switch ($type) { case 'string': diff --git a/src/S3/ApplyChecksumMiddleware.php b/src/S3/ApplyChecksumMiddleware.php index 79d2ae5478..c96ef2c7f0 100644 --- a/src/S3/ApplyChecksumMiddleware.php +++ b/src/S3/ApplyChecksumMiddleware.php @@ -147,9 +147,9 @@ private function addAlgorithmHeader( * @param CommandInterface $command * @return bool */ - private function isS3Express($command): bool + private function isS3Express(CommandInterface $command): bool { - $authSchemes = $command->getAuthSchemes(); - return isset($authSchemes['name']) && $authSchemes['name'] == 's3express'; + return isset($command['@context']['signing_service']) + && $command['@context']['signing_service'] === 's3express'; } } diff --git a/src/S3/S3Client.php b/src/S3/S3Client.php index 88de7473f7..038b582eb9 100644 --- a/src/S3/S3Client.php +++ b/src/S3/S3Client.php @@ -502,9 +502,9 @@ public function createPresignedRequest(CommandInterface $command, $expires, arra $command = clone $command; $command->getHandlerList()->remove('signer'); $request = \Aws\serialize($command); - $signing_name = empty($command->getAuthSchemes()) - ? $this->getSigningName($request->getUri()->getHost()) - : $command->getAuthSchemes()['name']; + $signing_name = isset($command['@context']['signing_service']) + ? $command['@context']['signing_service'] + : $this->getSigningName($request->getUri()->getHost()); $signature_version = $this->getSignatureVersionFromCommand($command); /** @var \Aws\Signature\SignatureInterface $signer */ @@ -723,12 +723,10 @@ private function getDisableExpressSessionAuthMiddleware() CommandInterface $command, RequestInterface $request = null ) use ($handler) { - if (!empty($command->getAuthSchemes()['version'] ) - && $command->getAuthSchemes()['version'] == 'v4-s3express' + if (!empty($command['@context']['signature_version']) + && $command['@context']['signature_version'] === 'v4-s3express' ) { - $authScheme = $command->getAuthSchemes(); - $authScheme['version'] = 's3v4'; - $command->setAuthSchemes($authScheme); + $command['@context']['signature_version'] = 's3v4'; } return $handler($command, $request); }; @@ -1111,10 +1109,8 @@ public static function addDocExamples($examples) */ private function getSignatureVersionFromCommand(CommandInterface $command) { - $signatureVersion = empty($command->getAuthSchemes()) - ? $this->getConfig('signature_version') - : $command->getAuthSchemes()['version']; - return $signatureVersion; + return isset($command['@context']['signature_version']) + ? $command['@context']['signature_version'] + : $this->getConfig('signature_version'); } - } diff --git a/src/Token/Token.php b/src/Token/Token.php index 6d2c566982..51e83c3688 100644 --- a/src/Token/Token.php +++ b/src/Token/Token.php @@ -1,13 +1,14 @@ createMock(AwsCredentialIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider); + $this->assertEquals('v4', $resolver->selectAuthScheme(['aws.auth#sigv4'])); + } + + public function testAcceptsCustomSchemeMap() + { + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $customMap = ['custom.auth#example' => 'v4']; + $resolver = new AuthSchemeResolver($credentialProvider, null, $customMap); + $this->assertEquals('v4', $resolver->selectAuthScheme(['custom.auth#example'])); + } + + /** + * @dataProvider schemeForIdentityProvider + */ + public function testSelectAuthSchemeReturnsCorrectSchemeForIdentity( + $authScheme, + $expectedSignatureVersion, + $args = [] + ){ + if ($expectedSignatureVersion === 'v4a' + && !extension_loaded('awscrt') + ) { + $this->markTestSkipped(); + } + + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $tokenProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(BearerTokenIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider, $tokenProvider); + $this->assertEquals($expectedSignatureVersion, $resolver->selectAuthScheme($authScheme, $args)); + } + + public function schemeForIdentityProvider() + { + return [ + [ + ['smithy.api#httpBearerAuth'], + 'bearer' + ] , + [ + ['aws.auth#sigv4'], + 'v4' + ], + [ + ['aws.auth#sigv4'], + 'v4-unsigned-body', + ['unsigned_payload' => true] + ], + [ + ['aws.auth#sigv4a'], + 'v4a' + ], + [ + ['smithy.auth#noAuth'], + 'anonymous' + ], + ]; + } + + public function testSelectAuthSchemeThrowsExceptionWhenNoCompatibleScheme() + { + $this->expectException(UnresolvedAuthSchemeException::class); + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider); + $resolver->selectAuthScheme(['non.existent#scheme']); + } + + public function testSelectAuthSchemePrioritizesFirstCompatibleScheme() + { + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider); + $this->assertEquals('v4', $resolver->selectAuthScheme(['aws.auth#sigv4', 'aws.auth#sigv4a'])); + } + + public function testSelectAuthSchemeSkipsIncompatible() + { + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider); + $this->assertEquals( + 'v4', + $resolver->selectAuthScheme(['smithy.api#httpBearerAuth', 'aws.auth#sigv4']) + ); + } + + public function testIsCompatibleAuthSchemeReturnsTrueForValidScheme() + { + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider); + $reflection = new \ReflectionClass($resolver); + $method = $reflection->getMethod('isCompatibleAuthScheme'); + $method->setAccessible(true); + $this->assertTrue($method->invokeArgs($resolver, ['v4'])); + } + + public function testIsCompatibleAuthSchemeReturnsFalseForInvalidScheme() + { + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $resolver = new AuthSchemeResolver($credentialProvider); + $reflection = new \ReflectionClass($resolver); + $method = $reflection->getMethod('isCompatibleAuthScheme'); + $method->setAccessible(true); + $this->assertFalse($method->invokeArgs($resolver, ['invalidScheme'])); + } +} diff --git a/tests/Auth/AuthSelectionMiddlewareTest.php b/tests/Auth/AuthSelectionMiddlewareTest.php new file mode 100644 index 0000000000..5cfbe7d51e --- /dev/null +++ b/tests/Auth/AuthSelectionMiddlewareTest.php @@ -0,0 +1,356 @@ +markTestSkipped(); + } + + $nextHandler = function (CommandInterface $command) use ($expected) { + $this->assertEquals($expected, $command['@context']['signature_version']); + }; + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $authResolver = new AuthSchemeResolver($credentialProvider); + $service = $this->generateTestService($serviceAuth, $operationAuth, $unsignedPayload); + $client = $this->generateTestClient($service); + $command = $client->getCommand('fooOperation', ['FooParam' => 'bar']); + + $middleware = new AuthSelectionMiddleware($nextHandler, $authResolver, $service); + + if ($expected === 'error') { + $this->expectException(UnresolvedAuthSchemeException::class); + $this->expectExceptionMessage( + 'Could not resolve an authentication scheme: The service does not support `aws.auth#sigv4a` authentication.' + ); + } + $middleware($command); + } + + public function ResolvesAuthSchemeWithoutCRTProvider() + { + return [ + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + [], + 'v4', + ], + [ + ['aws.auth#sigv4a', 'aws.auth#sigv4'], + [], + 'v4' + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['aws.auth#sigv4a', 'aws.auth#sigv4'], + 'v4' + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['aws.auth#sigv4a', 'aws.auth#sigv4'], + 'v4-unsigned-body', + true + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['smithy.auth#noAuth'], + 'anonymous' + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['aws.auth#sigv4a'], + 'error' + ], + ]; + } + + /** + * @param $serviceAuth + * @param $operationAuth + * @param $expected + * + * @dataProvider ResolvesAuthSchemeWithCRTprovider + */ + public function testResolvesAuthSchemeWithCRT( + $serviceAuth, + $operationAuth, + $expected, + $unsignedPayload = null + ) + { + if (!extension_loaded('awscrt')) { + $this->markTestSkipped(); + } + + $nextHandler = function (CommandInterface $command) use ($expected) { + $this->assertEquals($expected, $command['@context']['signature_version']); + }; + $service = $this->generateTestService($serviceAuth, $operationAuth, $unsignedPayload); + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $authResolver = new AuthSchemeResolver($credentialProvider); + $client = $this->generateTestClient($service); + $command = $client->getCommand('fooOperation', ['FooParam' => 'bar']); + + $middleware = new AuthSelectionMiddleware($nextHandler, $authResolver, $service); + + $middleware($command); + } + + public function ResolvesAuthSchemeWithCRTprovider() + { + return [ + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + [], + 'v4' + ], + [ + ['aws.auth#sigv4a', 'aws.auth#sigv4'], + [], + 'v4a' + ], + [ + ['aws.auth#sigv4'], + ['aws.auth#sigv4a'], + 'v4a' + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['smithy.auth#noAuth'], + 'anonymous' + ], + [ + ['aws.auth#sigv4a'], + ['aws.auth#sigv4'], + 'v4-unsigned-body', + true + ] + ]; + } + + /** + * @param $serviceAuth + * @param $operationAuth + * @param $identity + * @param $expected + * + * @dataProvider resolvesBearerAuthSchemeProvider + */ + public function testResolvesBearerAuthScheme( + $serviceAuth, + $operationAuth, + $tokenProvider, + $expected + ){ + $nextHandler = function (CommandInterface $command) use ($expected) { + $this->assertEquals($expected, $command['@context']['signature_version']); + }; + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $authResolver = new AuthSchemeResolver($credentialProvider, $tokenProvider); + $service = $this->generateTestService($serviceAuth, $operationAuth); + $client = $this->generateTestClient($service); + $command = $client->getCommand('fooOperation', ['FooParam' => 'bar']); + + $middleware = new AuthSelectionMiddleware($nextHandler, $authResolver, $service); + + if ($expected === 'error') { + $this->expectException(UnresolvedAuthSchemeException::class); + $this->expectExceptionMessage( + 'Could not resolve an authentication scheme: The service does not support `smithy.api#httpBearerAuth` authentication' + ); + } + + $middleware($command); + } + + public function resolvesBearerAuthSchemeProvider() + { + return [ + [ + ['smithy.api#httpBearerAuth', 'aws.auth#sigv4'], + [], + function () { + return Promise\Create::promiseFor( + $this->createMock(BearerTokenIdentity::class) + ); + }, + 'bearer' + ], + [ + ['smithy.api#httpBearerAuth', 'aws.auth#sigv4'], + [], + function () { + return Promise\Create::promiseFor( + null + ); + }, + 'v4' + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['smithy.api#httpBearerAuth'], + function () { + return Promise\Create::promiseFor( + $this->createMock(BearerTokenIdentity::class) + ); + }, + 'bearer' + ], + [ + ['aws.auth#sigv4', 'aws.auth#sigv4a'], + ['smithy.api#httpBearerAuth'], + function () { + return Promise\Create::promiseFor( + null + ); + }, + 'error' + ], + ]; + } + + public function testUnknownAuthSchemeThrows() + { + $this->expectException(UnresolvedAuthSchemeException::class); + $this->expectExceptionMessage( + 'Could not resolve an authentication scheme: The service does not support `notAnAuthScheme` authentication.' + ); + + $nextHandler = function (CommandInterface $command) { + return null; + }; + $service = $this->generateTestService(['notAnAuthScheme'], []); + $credentialProvider = function () { + return Promise\Create::promiseFor( + null + ); + }; + $authResolver = new AuthSchemeResolver($credentialProvider); + $client = $this->generateTestClient($service); + $command = $client->getCommand('fooOperation', ['FooParam' => 'bar']); + + $middleware = new AuthSelectionMiddleware($nextHandler, $authResolver, $service); + + $middleware($command); + } + + public function testCommandOverrideResolver() + { + $nextHandler = function (CommandInterface $command) { + $this->assertEquals('v4', $command['@context']['signature_version']); + }; + $service = $this->generateTestService(['v4'], []); + $credentialProvider = function () { + return Promise\Create::promiseFor( + $this->createMock(AwsCredentialIdentity::class) + ); + }; + $authResolver = new AuthSchemeResolver($credentialProvider, null, ['notanauthscheme' => 'foo']); + $client = $this->generateTestClient($service); + $command = $client->getCommand('fooOperation', ['FooParam' => 'bar']); + $command['@context']['auth_scheme_resolver'] = new AuthSchemeResolver($credentialProvider); + + $middleware = new AuthSelectionMiddleware($nextHandler, $authResolver, $service); + + $middleware($command); + } + + public function testMiddlewareAppliedAtInitialization() + { + $service = $this->generateTestService([], []); + $client = $this->generateTestClient($service); + $list = $client->getHandlerList(); + $this->assertStringContainsString('auth-selection', $list->__toString()); + } + + private function generateTestClient(Service $service, $args = []) + { + return new AwsClient( + array_merge( + [ + 'service' => 'foo', + 'api_provider' => function () use ($service) { + return $service->toArray(); + }, + 'region' => 'us-east-1', + 'version' => 'latest', + 'handler' => new MockHandler([new Result([])]) + ], + $args + ) + ); + } + + private function generateTestService($serviceAuth, $operationAuth, $unsignedPayload = false) + { + return new Service( + [ + 'metadata' => [ + "protocol" => "json", + "apiVersion" => "1989-08-05", + "jsonVersion" => "1.1", + "auth" => $serviceAuth + ], + 'operations' => [ + 'FooOperation' => [ + 'http' => [ + 'requestUri' => '/', + 'httpMethod' => 'POST' + ], + 'input' => [ + 'type' => 'structure', + 'members' => [ + 'FooParam' => [ + 'type' => 'string', + ], + ] + ], + 'auth' => $operationAuth, + 'unsignedpayload' => $unsignedPayload + ], + ] + ], + function () { return []; } + ); + } +} diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index beef196c60..9f47393a9c 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -2,6 +2,8 @@ namespace Aws\Test; use Aws\Api\Service; +use Aws\Auth\AuthSchemeResolver; +use Aws\Auth\AuthSchemeResolverInterface; use Aws\ClientResolver; use Aws\ClientSideMonitoring\Configuration; use Aws\ClientSideMonitoring\ConfigurationProvider; @@ -1574,4 +1576,19 @@ public function testIgnoreConfiguredEndpointUrls() putenv('AWS_ENDPOINT_URL' . '='); putenv('AWS_ENDPOINT_URL_S3' . '='); } + + public function testAppliesAuthSchemeResolver() + { + $r = new ClientResolver(ClientResolver::getDefaultArguments()); + $conf = $r->resolve([ + 'service' => 'dynamodb', + 'region' => 'x', + 'version' => 'latest', + ], new HandlerList()); + $this->assertArrayHasKey('auth_scheme_resolver', $conf); + $this->assertInstanceOf( + AuthSchemeResolverInterface::class, + $conf['auth_scheme_resolver'] + ); + } } diff --git a/tests/CommandTest.php b/tests/CommandTest.php index e199b671be..0b6ef9deec 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -3,7 +3,7 @@ use Aws\Command; use Aws\HandlerList; -use PHPUnit\Framework\TestCase; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** * @covers Aws\Command @@ -85,4 +85,16 @@ public function testCanAccessLikeArray() $this->assertArrayNotHasKey('boo', $c); $this->assertNull($c['boo']); } + + public function testGetAuthSchemesEmitsDeprecationNotice() + { + $this->expectDeprecation(E_USER_DEPRECATED); + $this->expectDeprecationMessage( + 'Aws\Command::getAuthSchemes is deprecated. Auth schemes resolved using the service' + .' `auth` trait or via endpoint resolution can now be found in the command `@context` property.' + ); + + $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); + $c->getAuthSchemes(); + } } diff --git a/tests/Credentials/CredentialsTest.php b/tests/Credentials/CredentialsTest.php index 248afd5633..5b630c2fdc 100644 --- a/tests/Credentials/CredentialsTest.php +++ b/tests/Credentials/CredentialsTest.php @@ -2,6 +2,9 @@ namespace Aws\Test\Credentials; use Aws\Credentials\Credentials; +use Aws\Identity\AwsCredentialIdentity; +use Aws\Identity\AwsCredentialIdentityInterface; +use Aws\Identity\IdentityInterface; use PHPUnit\Framework\TestCase; /** @@ -57,4 +60,10 @@ public function testSerialization() 'expires' => 10, ], $actual); } + + public function testIsInstanceOfIdentity() + { + $credentials = new Credentials('key-value', 'secret-value'); + $this->assertInstanceOf(AwsCredentialIdentity::class, $credentials); + } } diff --git a/tests/EndpointV2/EndpointProviderV2Test.php b/tests/EndpointV2/EndpointProviderV2Test.php index 29b7b3513e..196b62ce65 100644 --- a/tests/EndpointV2/EndpointProviderV2Test.php +++ b/tests/EndpointV2/EndpointProviderV2Test.php @@ -275,23 +275,22 @@ public function testRulesetProtocolEndpointAndErrorCases($service, $clientArgs, $expectedVersion = str_replace('sig', '', $expectedAuthScheme['name']); } $this->assertEquals( - $expectedVersion, - $cmd->getAuthSchemes()['version'] + $cmd['@context']['signature_version'], + $expectedVersion ); $this->assertEquals( - $expectedAuthScheme['signingName'], - $cmd->getAuthSchemes()['name'] + $cmd['@context']['signing_service'], + $expectedAuthScheme['signingName'] ); - if (isset($cmd->getAuthSchemes()['region'])) { + if (isset($cmd['@context']['signing_region'])) { $this->assertEquals( - $expectedAuthScheme['signingRegion'], - $cmd->getAuthSchemes()['region'] + $cmd['@context']['signing_region'], + $expectedAuthScheme['signingRegion'] ); - } elseif (isset($cmd->getAuthSchemes['signingRegionSet'])) { + } elseif (isset($cmd['@context']['signing_region_set'])) { $this->assertEquals( - $expectedAuthScheme['signingRegionSet'], - $cmd->getAuthSchemes()['region'] - ); + $cmd['@context']['signing_region_set'], + $expectedAuthScheme['signingRegionSet']); } } } diff --git a/tests/EndpointV2/EndpointV2MiddlewareTest.php b/tests/EndpointV2/EndpointV2MiddlewareTest.php index 309073ae2c..79921d0800 100644 --- a/tests/EndpointV2/EndpointV2MiddlewareTest.php +++ b/tests/EndpointV2/EndpointV2MiddlewareTest.php @@ -3,7 +3,6 @@ use Aws\Api\Service; use Aws\Auth\Exception\UnresolvedAuthSchemeException; -use Aws\Endpoint\PartitionEndpointProvider; use Aws\EndpointV2\EndpointProviderV2; use Aws\EndpointV2\EndpointV2Middleware; use Aws\EndpointV2\Ruleset\RulesetEndpoint; @@ -35,10 +34,6 @@ public function testSuccessfullyResolvesEndpointAndAuthScheme( $nextHandler = function ($command, $endpoint) use ($service, $expectedUri) { $this->assertInstanceOf(RulesetEndpoint::class, $endpoint); $this->assertEquals($expectedUri, $endpoint->getUrl()); - - if (!empty($endpoint->getProperty('authSchemes'))) { - self::assertNotEmpty($command->getAuthSchemes()); - } }; $client = $this->getTestClient($service, $clientArgs); @@ -274,18 +269,4 @@ public function testBadParametersOnInvocation() { $middleware = new EndpointV2Middleware($nextHandler, $endpointProvider, $api, $args); $middleware('not_a_command'); } - - public function testMiddlewareAppliedForEndpointV2Clients() - { - $client = $this->getTestClient('s3'); - $list = $client->getHandlerList(); - $this->assertStringContainsString('endpoint-resolution', $list->__toString()); - } - - public function testMiddlewareNotAppliedForNonEndpointV2Clients() - { - $client = $this->getTestClient('s3', ['endpoint_provider' => PartitionEndpointProvider::defaultProvider()]); - $list = $client->getHandlerList(); - $this->assertStringNotContainsString('endpoint-resolution', $list->__toString()); - } } diff --git a/tests/Token/TokenTest.php b/tests/Token/TokenTest.php new file mode 100644 index 0000000000..621678c8a1 --- /dev/null +++ b/tests/Token/TokenTest.php @@ -0,0 +1,61 @@ +assertSame('foo', $token->getToken()); + $this->assertSame($exp, $token->getExpiration()); + $this->assertEquals([ + 'token' => 'foo', + 'expires' => $exp + ], $token->toArray()); + } + + public function testDeterminesIfExpired() + { + $this->assertFalse((new Token('foo'))->isExpired()); + $this->assertFalse( + (new Token('foo', time() + 100))->isExpired() + ); + $this->assertTrue( + (new Token('foo', time() - 1000))->isExpired() + ); + } + + public function testSerialization() + { + $token = new Token('token-value'); + $actual = unserialize(serialize($token))->toArray(); + $this->assertEquals([ + 'token' => 'token-value', + 'expires' => null, + ], $actual); + + $token = new Token('token-value', 10); + $actual = unserialize(serialize($token))->toArray(); + + $this->assertEquals([ + 'token' => 'token-value', + 'expires' => 10, + ], $actual); + } + + public function testIsInstanceOfIdentity() + { + $token = new Token('token-value'); + $this->assertInstanceOf(BearerTokenIdentity::class, $token); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3591ad7425..d4ec2d3a0f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -34,3 +34,5 @@ class_alias('\PHPUnit_Framework_Error_Warning', '\PHPUnit\Framework\Error\Warnin $patchGeneratorPath = __DIR__ . '/bootstrap/PHPUnit_Framework_MockObject_Generator_7.4.php'; file_put_contents($vendorGeneratorPath, file_get_contents($patchGeneratorPath)); } + +