EndpointV2Middleware.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. namespace Aws\EndpointV2;
  3. use Aws\Api\Operation;
  4. use Aws\Api\Service;
  5. use Aws\Auth\Exception\UnresolvedAuthSchemeException;
  6. use Aws\CommandInterface;
  7. use Closure;
  8. use GuzzleHttp\Promise\Promise;
  9. use function JmesPath\search;
  10. /**
  11. * Handles endpoint rule evaluation and endpoint resolution.
  12. *
  13. * IMPORTANT: this middleware must be added to the "build" step.
  14. * Specifically, it must precede the 'builder' step.
  15. *
  16. * @internal
  17. */
  18. class EndpointV2Middleware
  19. {
  20. const ACCOUNT_ID_PARAM = 'AccountId';
  21. const ACCOUNT_ID_ENDPOINT_MODE_PARAM = 'AccountIdEndpointMode';
  22. private static $validAuthSchemes = [
  23. 'sigv4' => 'v4',
  24. 'sigv4a' => 'v4a',
  25. 'none' => 'anonymous',
  26. 'bearer' => 'bearer',
  27. 'sigv4-s3express' => 'v4-s3express'
  28. ];
  29. /** @var callable */
  30. private $nextHandler;
  31. /** @var EndpointProviderV2 */
  32. private $endpointProvider;
  33. /** @var Service */
  34. private $api;
  35. /** @var array */
  36. private $clientArgs;
  37. /** @var Closure */
  38. private $credentialProvider;
  39. /**
  40. * Create a middleware wrapper function
  41. *
  42. * @param EndpointProviderV2 $endpointProvider
  43. * @param Service $api
  44. * @param array $args
  45. * @param callable $credentialProvider
  46. *
  47. * @return Closure
  48. */
  49. public static function wrap(
  50. EndpointProviderV2 $endpointProvider,
  51. Service $api,
  52. array $args,
  53. callable $credentialProvider
  54. ) : Closure
  55. {
  56. return function (callable $handler) use ($endpointProvider, $api, $args, $credentialProvider) {
  57. return new self($handler, $endpointProvider, $api, $args, $credentialProvider);
  58. };
  59. }
  60. /**
  61. * @param callable $nextHandler
  62. * @param EndpointProviderV2 $endpointProvider
  63. * @param Service $api
  64. * @param array $args
  65. */
  66. public function __construct(
  67. callable $nextHandler,
  68. EndpointProviderV2 $endpointProvider,
  69. Service $api,
  70. array $args,
  71. callable $credentialProvider = null
  72. )
  73. {
  74. $this->nextHandler = $nextHandler;
  75. $this->endpointProvider = $endpointProvider;
  76. $this->api = $api;
  77. $this->clientArgs = $args;
  78. $this->credentialProvider = $credentialProvider;
  79. }
  80. /**
  81. * @param CommandInterface $command
  82. *
  83. * @return Promise
  84. */
  85. public function __invoke(CommandInterface $command)
  86. {
  87. $nextHandler = $this->nextHandler;
  88. $operation = $this->api->getOperation($command->getName());
  89. $commandArgs = $command->toArray();
  90. $providerArgs = $this->resolveArgs($commandArgs, $operation);
  91. $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs);
  92. if (!empty($authSchemes = $endpoint->getProperty('authSchemes'))) {
  93. $this->applyAuthScheme(
  94. $authSchemes,
  95. $command
  96. );
  97. }
  98. return $nextHandler($command, $endpoint);
  99. }
  100. /**
  101. * Resolves client, context params, static context params and endpoint provider
  102. * arguments provided at the command level.
  103. *
  104. * @param array $commandArgs
  105. * @param Operation $operation
  106. *
  107. * @return array
  108. */
  109. private function resolveArgs(array $commandArgs, Operation $operation): array
  110. {
  111. $rulesetParams = $this->endpointProvider->getRuleset()->getParameters();
  112. if (isset($rulesetParams[self::ACCOUNT_ID_PARAM])
  113. && isset($rulesetParams[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM])) {
  114. $this->clientArgs[self::ACCOUNT_ID_PARAM] = $this->resolveAccountId();
  115. }
  116. $endpointCommandArgs = $this->filterEndpointCommandArgs(
  117. $rulesetParams,
  118. $commandArgs
  119. );
  120. $staticContextParams = $this->bindStaticContextParams(
  121. $operation->getStaticContextParams()
  122. );
  123. $contextParams = $this->bindContextParams(
  124. $commandArgs, $operation->getContextParams()
  125. );
  126. $operationContextParams = $this->bindOperationContextParams(
  127. $commandArgs,
  128. $operation->getOperationContextParams()
  129. );
  130. return array_merge(
  131. $this->clientArgs,
  132. $operationContextParams,
  133. $contextParams,
  134. $staticContextParams,
  135. $endpointCommandArgs
  136. );
  137. }
  138. /**
  139. * Compares Ruleset parameters against Command arguments
  140. * to create a mapping of arguments to pass into the
  141. * endpoint provider for endpoint resolution.
  142. *
  143. * @param array $rulesetParams
  144. * @param array $commandArgs
  145. * @return array
  146. */
  147. private function filterEndpointCommandArgs(
  148. array $rulesetParams,
  149. array $commandArgs
  150. ): array
  151. {
  152. $endpointMiddlewareOpts = [
  153. '@use_dual_stack_endpoint' => 'UseDualStack',
  154. '@use_accelerate_endpoint' => 'Accelerate',
  155. '@use_path_style_endpoint' => 'ForcePathStyle'
  156. ];
  157. $filteredArgs = [];
  158. foreach($rulesetParams as $name => $value) {
  159. if (isset($commandArgs[$name])) {
  160. if (!empty($value->getBuiltIn())) {
  161. continue;
  162. }
  163. $filteredArgs[$name] = $commandArgs[$name];
  164. }
  165. }
  166. if ($this->api->getServiceName() === 's3') {
  167. foreach($endpointMiddlewareOpts as $optionName => $newValue) {
  168. if (isset($commandArgs[$optionName])) {
  169. $filteredArgs[$newValue] = $commandArgs[$optionName];
  170. }
  171. }
  172. }
  173. return $filteredArgs;
  174. }
  175. /**
  176. * Binds static context params to their corresponding values.
  177. *
  178. * @param $staticContextParams
  179. *
  180. * @return array
  181. */
  182. private function bindStaticContextParams($staticContextParams): array
  183. {
  184. $scopedParams = [];
  185. forEach($staticContextParams as $paramName => $paramValue) {
  186. $scopedParams[$paramName] = $paramValue['value'];
  187. }
  188. return $scopedParams;
  189. }
  190. /**
  191. * Binds context params to their corresponding values found in
  192. * command arguments.
  193. *
  194. * @param array $commandArgs
  195. * @param array $contextParams
  196. *
  197. * @return array
  198. */
  199. private function bindContextParams(
  200. array $commandArgs,
  201. array $contextParams
  202. ): array
  203. {
  204. $scopedParams = [];
  205. foreach($contextParams as $name => $spec) {
  206. if (isset($commandArgs[$spec['shape']])) {
  207. $scopedParams[$name] = $commandArgs[$spec['shape']];
  208. }
  209. }
  210. return $scopedParams;
  211. }
  212. /**
  213. * Binds context params to their corresponding values found in
  214. * command arguments.
  215. *
  216. * @param array $commandArgs
  217. * @param array $contextParams
  218. *
  219. * @return array
  220. */
  221. private function bindOperationContextParams(
  222. array $commandArgs,
  223. array $operationContextParams
  224. ): array
  225. {
  226. $scopedParams = [];
  227. foreach($operationContextParams as $name => $spec) {
  228. $scopedValue = search($spec['path'], $commandArgs);
  229. if ($scopedValue) {
  230. $scopedParams[$name] = $scopedValue;
  231. }
  232. }
  233. return $scopedParams;
  234. }
  235. /**
  236. * Applies resolved auth schemes to the command object.
  237. *
  238. * @param $authSchemes
  239. * @param $command
  240. *
  241. * @return void
  242. */
  243. private function applyAuthScheme(
  244. array $authSchemes,
  245. CommandInterface $command
  246. ): void
  247. {
  248. $authScheme = $this->resolveAuthScheme($authSchemes);
  249. $command['@context']['signature_version'] = $authScheme['version'];
  250. if (isset($authScheme['name'])) {
  251. $command['@context']['signing_service'] = $authScheme['name'];
  252. }
  253. if (isset($authScheme['region'])) {
  254. $command['@context']['signing_region'] = $authScheme['region'];
  255. } elseif (isset($authScheme['signingRegionSet'])) {
  256. $command['@context']['signing_region_set'] = $authScheme['signingRegionSet'];
  257. }
  258. }
  259. /**
  260. * Returns the first compatible auth scheme in an endpoint object's
  261. * auth schemes.
  262. *
  263. * @param array $authSchemes
  264. *
  265. * @return array
  266. */
  267. private function resolveAuthScheme(array $authSchemes): array
  268. {
  269. $invalidAuthSchemes = [];
  270. foreach($authSchemes as $authScheme) {
  271. if ($this->isValidAuthScheme($authScheme['name'])) {
  272. return $this->normalizeAuthScheme($authScheme);
  273. }
  274. $invalidAuthSchemes[$authScheme['name']] = false;
  275. }
  276. $invalidAuthSchemesString = '`' . implode(
  277. '`, `',
  278. array_keys($invalidAuthSchemes))
  279. . '`';
  280. $validAuthSchemesString = '`'
  281. . implode('`, `', array_keys(
  282. array_diff_key(self::$validAuthSchemes, $invalidAuthSchemes))
  283. )
  284. . '`';
  285. throw new UnresolvedAuthSchemeException(
  286. "This operation requests {$invalidAuthSchemesString}"
  287. . " auth schemes, but the client currently supports {$validAuthSchemesString}."
  288. );
  289. }
  290. /**
  291. * Normalizes an auth scheme's name, signing region or signing region set
  292. * to the auth keys recognized by the SDK.
  293. *
  294. * @param array $authScheme
  295. * @return array
  296. */
  297. private function normalizeAuthScheme(array $authScheme): array
  298. {
  299. /*
  300. sigv4a will contain a regionSet property. which is guaranteed to be `*`
  301. for now. The SigV4 class handles this automatically for now. It seems
  302. complexity will be added here in the future.
  303. */
  304. $normalizedAuthScheme = [];
  305. if (isset($authScheme['disableDoubleEncoding'])
  306. && $authScheme['disableDoubleEncoding'] === true
  307. && $authScheme['name'] !== 'sigv4a'
  308. && $authScheme['name'] !== 'sigv4-s3express'
  309. ) {
  310. $normalizedAuthScheme['version'] = 's3v4';
  311. } else {
  312. $normalizedAuthScheme['version'] = self::$validAuthSchemes[$authScheme['name']];
  313. }
  314. $normalizedAuthScheme['name'] = $authScheme['signingName'] ?? null;
  315. $normalizedAuthScheme['region'] = $authScheme['signingRegion'] ?? null;
  316. $normalizedAuthScheme['signingRegionSet'] = $authScheme['signingRegionSet'] ?? null;
  317. return $normalizedAuthScheme;
  318. }
  319. private function isValidAuthScheme($signatureVersion): bool
  320. {
  321. if (isset(self::$validAuthSchemes[$signatureVersion])) {
  322. if ($signatureVersion === 'sigv4a') {
  323. return extension_loaded('awscrt');
  324. }
  325. return true;
  326. }
  327. return false;
  328. }
  329. /**
  330. * This method tries to resolve an `AccountId` parameter from a resolved identity.
  331. * We will just perform this operation if the parameter `AccountId` is part of the ruleset parameters and
  332. * `AccountIdEndpointMode` is not disabled, otherwise, we will ignore it.
  333. *
  334. * @return null|string
  335. */
  336. private function resolveAccountId(): ?string
  337. {
  338. if (isset($this->clientArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM])
  339. && $this->clientArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM] === 'disabled') {
  340. return null;
  341. }
  342. if (is_null($this->credentialProvider)) {
  343. return null;
  344. }
  345. $identityProviderFn = $this->credentialProvider;
  346. $identity = $identityProviderFn()->wait();
  347. return $identity->getAccountId();
  348. }
  349. }