BucketEndpointArnMiddleware.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. <?php
  2. namespace Aws\S3;
  3. use Aws\Api\Service;
  4. use Aws\Arn\AccessPointArnInterface;
  5. use Aws\Arn\ArnParser;
  6. use Aws\Arn\Exception\InvalidArnException;
  7. use Aws\Arn\AccessPointArn as BaseAccessPointArn;
  8. use Aws\Arn\S3\OutpostsAccessPointArn;
  9. use Aws\Arn\S3\OutpostsArnInterface;
  10. use Aws\CommandInterface;
  11. use Aws\Endpoint\PartitionEndpointProvider;
  12. use Aws\Exception\InvalidRegionException;
  13. use Aws\Exception\UnresolvedEndpointException;
  14. use Aws\S3\Exception\S3Exception;
  15. use Psr\Http\Message\RequestInterface;
  16. /**
  17. * Checks for access point ARN in members targeting BucketName, modifying
  18. * endpoint as appropriate
  19. *
  20. * @internal
  21. */
  22. class BucketEndpointArnMiddleware
  23. {
  24. use EndpointRegionHelperTrait;
  25. /** @var callable */
  26. private $nextHandler;
  27. /** @var array */
  28. private $nonArnableCommands = ['CreateBucket'];
  29. /**
  30. * Create a middleware wrapper function.
  31. *
  32. * @param Service $service
  33. * @param $region
  34. * @param array $config
  35. * @return callable
  36. */
  37. public static function wrap(
  38. Service $service,
  39. $region,
  40. array $config
  41. ) {
  42. return function (callable $handler) use ($service, $region, $config) {
  43. return new self($handler, $service, $region, $config);
  44. };
  45. }
  46. public function __construct(
  47. callable $nextHandler,
  48. Service $service,
  49. $region,
  50. array $config = []
  51. ) {
  52. $this->partitionProvider = PartitionEndpointProvider::defaultProvider();
  53. $this->region = $region;
  54. $this->service = $service;
  55. $this->config = $config;
  56. $this->nextHandler = $nextHandler;
  57. }
  58. public function __invoke(CommandInterface $cmd, RequestInterface $req)
  59. {
  60. $nextHandler = $this->nextHandler;
  61. $op = $this->service->getOperation($cmd->getName())->toArray();
  62. if (!empty($op['input']['shape'])) {
  63. $service = $this->service->toArray();
  64. if (!empty($input = $service['shapes'][$op['input']['shape']])) {
  65. foreach ($input['members'] as $key => $member) {
  66. if ($member['shape'] === 'BucketName') {
  67. $arnableKey = $key;
  68. break;
  69. }
  70. }
  71. if (!empty($arnableKey) && ArnParser::isArn($cmd[$arnableKey])) {
  72. try {
  73. // Throw for commands that do not support ARN inputs
  74. if (in_array($cmd->getName(), $this->nonArnableCommands)) {
  75. throw new S3Exception(
  76. 'ARN values cannot be used in the bucket field for'
  77. . ' the ' . $cmd->getName() . ' operation.',
  78. $cmd
  79. );
  80. }
  81. $arn = ArnParser::parse($cmd[$arnableKey]);
  82. $partition = $this->validateArn($arn);
  83. $host = $this->generateAccessPointHost($arn, $req);
  84. // Remove encoded bucket string from path
  85. $path = $req->getUri()->getPath();
  86. $encoded = rawurlencode($cmd[$arnableKey]);
  87. $len = strlen($encoded) + 1;
  88. if (substr($path, 0, $len) === "/{$encoded}") {
  89. $path = substr($path, $len);
  90. }
  91. if (empty($path)) {
  92. $path = '';
  93. }
  94. // Set modified request
  95. $req = $req->withUri(
  96. $req->getUri()->withHost($host)->withPath($path)
  97. );
  98. // Update signing region based on ARN data if configured to do so
  99. if ($this->config['use_arn_region']->isUseArnRegion()) {
  100. $region = $arn->getRegion();
  101. } else {
  102. $region = $this->region;
  103. }
  104. $endpointData = $partition([
  105. 'region' => $region,
  106. 'service' => $arn->getService()
  107. ]);
  108. $cmd['@context']['signing_region'] = $endpointData['signingRegion'];
  109. // Update signing service for Outposts ARNs
  110. if ($arn instanceof OutpostsArnInterface) {
  111. $cmd['@context']['signing_service'] = $arn->getService();
  112. }
  113. } catch (InvalidArnException $e) {
  114. // Add context to ARN exception
  115. throw new S3Exception(
  116. 'Bucket parameter parsed as ARN and failed with: '
  117. . $e->getMessage(),
  118. $cmd,
  119. [],
  120. $e
  121. );
  122. }
  123. }
  124. }
  125. }
  126. return $nextHandler($cmd, $req);
  127. }
  128. private function generateAccessPointHost(
  129. BaseAccessPointArn $arn,
  130. RequestInterface $req
  131. ) {
  132. if ($arn instanceof OutpostsAccessPointArn) {
  133. $accesspointName = $arn->getAccesspointName();
  134. } else {
  135. $accesspointName = $arn->getResourceId();
  136. }
  137. $host = "{$accesspointName}-" . $arn->getAccountId();
  138. if ($arn instanceof OutpostsAccessPointArn) {
  139. $host .= '.' . $arn->getOutpostId() . '.s3-outposts';
  140. } else {
  141. $host .= '.s3-accesspoint';
  142. if (!empty($this->config['dual_stack'])) {
  143. $host .= '.dualstack';
  144. }
  145. }
  146. if (!empty($this->config['use_arn_region']->isUseArnRegion())) {
  147. $region = $arn->getRegion();
  148. } else {
  149. $region = $this->region;
  150. }
  151. $host .= '.' . $region . '.' . $this->getPartitionSuffix($arn, $this->partitionProvider);
  152. return $host;
  153. }
  154. /**
  155. * Validates an ARN, returning a partition object corresponding to the ARN
  156. * if successful
  157. *
  158. * @param $arn
  159. * @return \Aws\Endpoint\Partition
  160. */
  161. private function validateArn($arn)
  162. {
  163. if ($arn instanceof AccessPointArnInterface) {
  164. // Dualstack is not supported with Outposts access points
  165. if ($arn instanceof OutpostsAccessPointArn
  166. && !empty($this->config['dual_stack'])
  167. ) {
  168. throw new UnresolvedEndpointException(
  169. 'Dualstack is currently not supported with S3 Outposts access'
  170. . ' points. Please disable dualstack or do not supply an'
  171. . ' access point ARN.');
  172. }
  173. // Accelerate is not supported with access points
  174. if (!empty($this->config['accelerate'])) {
  175. throw new UnresolvedEndpointException(
  176. 'Accelerate is currently not supported with access points.'
  177. . ' Please disable accelerate or do not supply an access'
  178. . ' point ARN.');
  179. }
  180. // Path-style is not supported with access points
  181. if (!empty($this->config['path_style'])) {
  182. throw new UnresolvedEndpointException(
  183. 'Path-style addressing is currently not supported with'
  184. . ' access points. Please disable path-style or do not'
  185. . ' supply an access point ARN.');
  186. }
  187. // Custom endpoint is not supported with access points
  188. if (!is_null($this->config['endpoint'])) {
  189. throw new UnresolvedEndpointException(
  190. 'A custom endpoint has been supplied along with an access'
  191. . ' point ARN, and these are not compatible with each other.'
  192. . ' Please only use one or the other.');
  193. }
  194. // Get partitions for ARN and client region
  195. $arnPart = $this->partitionProvider->getPartition(
  196. $arn->getRegion(),
  197. 's3'
  198. );
  199. $clientPart = $this->partitionProvider->getPartition(
  200. $this->region,
  201. 's3'
  202. );
  203. // If client partition not found, try removing pseudo-region qualifiers
  204. if (!($clientPart->isRegionMatch($this->region, 's3'))) {
  205. $clientPart = $this->partitionProvider->getPartition(
  206. $this->stripPseudoRegions($this->region),
  207. 's3'
  208. );
  209. }
  210. // Verify that the partition matches for supplied partition and region
  211. if ($arn->getPartition() !== $clientPart->getName()) {
  212. throw new InvalidRegionException('The supplied ARN partition'
  213. . " does not match the client's partition.");
  214. }
  215. if ($clientPart->getName() !== $arnPart->getName()) {
  216. throw new InvalidRegionException('The corresponding partition'
  217. . ' for the supplied ARN region does not match the'
  218. . " client's partition.");
  219. }
  220. // Ensure ARN region matches client region unless
  221. // configured for using ARN region over client region
  222. $this->validateMatchingRegion($arn);
  223. // Ensure it is not resolved to fips pseudo-region for S3 Outposts
  224. $this->validateFipsNotUsedWithOutposts($arn);
  225. return $arnPart;
  226. }
  227. throw new InvalidArnException('Provided ARN was not a valid S3 access'
  228. . ' point ARN or S3 Outposts access point ARN.');
  229. }
  230. }