EndpointArnMiddleware.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <?php
  2. namespace Aws\S3Control;
  3. use Aws\Api\Service;
  4. use Aws\Arn\AccessPointArnInterface;
  5. use Aws\Arn\ArnInterface;
  6. use Aws\Arn\ArnParser;
  7. use Aws\Arn\Exception\InvalidArnException;
  8. use Aws\Arn\S3\BucketArnInterface;
  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\EndpointRegionHelperTrait;
  15. use GuzzleHttp\Psr7;
  16. use Psr\Http\Message\RequestInterface;
  17. /**
  18. * Checks for access point ARN in members targeting BucketName, modifying
  19. * endpoint as appropriate
  20. *
  21. * @internal
  22. */
  23. class EndpointArnMiddleware
  24. {
  25. use EndpointRegionHelperTrait;
  26. /**
  27. * Commands which do not do ARN expansion for a specific given shape name
  28. * @var array
  29. */
  30. private static $selectiveNonArnableCmds = [
  31. 'AccessPointName' => [
  32. 'CreateAccessPoint',
  33. ],
  34. 'BucketName' => [],
  35. ];
  36. /**
  37. * Commands which do not do ARN expansion at all for relevant members
  38. * @var array
  39. */
  40. private static $nonArnableCmds = [
  41. 'CreateBucket',
  42. 'ListRegionalBuckets',
  43. ];
  44. /**
  45. * Commands which trigger endpoint and signer redirection based on presence
  46. * of OutpostId
  47. * @var array
  48. */
  49. private static $outpostIdRedirectCmds = [
  50. 'CreateBucket',
  51. 'ListRegionalBuckets',
  52. ];
  53. /** @var callable */
  54. private $nextHandler;
  55. /**
  56. * Create a middleware wrapper function.
  57. *
  58. * @param Service $service
  59. * @param $region
  60. * @param array $config
  61. * @return callable
  62. */
  63. public static function wrap(
  64. Service $service,
  65. $region,
  66. array $config
  67. ) {
  68. return function (callable $handler) use ($service, $region, $config) {
  69. return new self($handler, $service, $region, $config);
  70. };
  71. }
  72. public function __construct(
  73. callable $nextHandler,
  74. Service $service,
  75. $region,
  76. array $config = []
  77. ) {
  78. $this->partitionProvider = PartitionEndpointProvider::defaultProvider();
  79. $this->region = $region;
  80. $this->service = $service;
  81. $this->config = $config;
  82. $this->nextHandler = $nextHandler;
  83. }
  84. public function __invoke(CommandInterface $cmd, RequestInterface $req)
  85. {
  86. $nextHandler = $this->nextHandler;
  87. $op = $this->service->getOperation($cmd->getName())->toArray();
  88. if (!empty($op['input']['shape'])
  89. && !in_array($cmd->getName(), self::$nonArnableCmds)
  90. ) {
  91. $service = $this->service->toArray();
  92. if (!empty($input = $service['shapes'][$op['input']['shape']])) {
  93. // Stores member name that targets 'BucketName' shape
  94. $bucketNameMember = null;
  95. // Stores member name that targets 'AccessPointName' shape
  96. $accesspointNameMember = null;
  97. foreach ($input['members'] as $key => $member) {
  98. if ($member['shape'] === 'BucketName') {
  99. $bucketNameMember = $key;
  100. }
  101. if ($member['shape'] === 'AccessPointName') {
  102. $accesspointNameMember = $key;
  103. }
  104. }
  105. // Determine if appropriate member contains ARN value and is
  106. // eligible for ARN expansion
  107. if (!is_null($bucketNameMember)
  108. && !empty($cmd[$bucketNameMember])
  109. && !in_array($cmd->getName(), self::$selectiveNonArnableCmds['BucketName'])
  110. && ArnParser::isArn($cmd[$bucketNameMember])
  111. ) {
  112. $arn = ArnParser::parse($cmd[$bucketNameMember]);
  113. $partition = $this->validateBucketArn($arn);
  114. } elseif (!is_null($accesspointNameMember)
  115. && !empty($cmd[$accesspointNameMember])
  116. && !in_array($cmd->getName(), self::$selectiveNonArnableCmds['AccessPointName'])
  117. && ArnParser::isArn($cmd[$accesspointNameMember])
  118. ) {
  119. $arn = ArnParser::parse($cmd[$accesspointNameMember]);
  120. $partition = $this->validateAccessPointArn($arn);
  121. }
  122. // Process only if an appropriate member contains an ARN value
  123. // and is an Outposts ARN
  124. if (!empty($arn) && $arn instanceof OutpostsArnInterface) {
  125. // Generate host based on ARN
  126. $host = $this->generateOutpostsArnHost($arn, $req);
  127. $req = $req->withHeader('x-amz-outpost-id', $arn->getOutpostId());
  128. // ARN replacement
  129. $path = $req->getUri()->getPath();
  130. if ($arn instanceof AccessPointArnInterface) {
  131. // Replace ARN with access point name
  132. $path = str_replace(
  133. urlencode($cmd[$accesspointNameMember]),
  134. $arn->getAccesspointName(),
  135. $path
  136. );
  137. // Replace ARN in the payload
  138. $req->getBody()->seek(0);
  139. $body = Psr7\stream_for(str_replace(
  140. $cmd[$accesspointNameMember],
  141. $arn->getAccesspointName(),
  142. $req->getBody()->getContents()
  143. ));
  144. // Replace ARN in the command
  145. $cmd[$accesspointNameMember] = $arn->getAccesspointName();
  146. } elseif ($arn instanceof BucketArnInterface) {
  147. // Replace ARN in the path
  148. $path = str_replace(
  149. urlencode($cmd[$bucketNameMember]),
  150. $arn->getBucketName(),
  151. $path
  152. );
  153. // Replace ARN in the payload
  154. $req->getBody()->seek(0);
  155. $newBody = str_replace(
  156. $cmd[$bucketNameMember],
  157. $arn->getBucketName(),
  158. $req->getBody()->getContents()
  159. );
  160. $body = Psr7\stream_for($newBody);
  161. // Replace ARN in the command
  162. $cmd[$bucketNameMember] = $arn->getBucketName();
  163. }
  164. // Validate or set account ID in command
  165. if (isset($cmd['AccountId'])) {
  166. if ($cmd['AccountId'] !== $arn->getAccountId()) {
  167. throw new \InvalidArgumentException("The account ID"
  168. . " supplied in the command ({$cmd['AccountId']})"
  169. . " does not match the account ID supplied in the"
  170. . " ARN (" . $arn->getAccountId() . ").");
  171. }
  172. } else {
  173. $cmd['AccountId'] = $arn->getAccountId();
  174. }
  175. // Set modified request
  176. $req = $req
  177. ->withUri($req->getUri()->withHost($host)->withPath($path))
  178. ->withHeader('x-amz-account-id', $arn->getAccountId());
  179. if (isset($body)) {
  180. $req = $req->withBody($body);
  181. }
  182. // Update signing region based on ARN data if configured to do so
  183. if ($this->config['use_arn_region']->isUseArnRegion()) {
  184. $region = $arn->getRegion();
  185. } else {
  186. $region = $this->region;
  187. }
  188. $endpointData = $partition([
  189. 'region' => $region,
  190. 'service' => $arn->getService()
  191. ]);
  192. $cmd['@context']['signing_region'] = $endpointData['signingRegion'];
  193. // Update signing service for Outposts ARNs
  194. if ($arn instanceof OutpostsArnInterface) {
  195. $cmd['@context']['signing_service'] = $arn->getService();
  196. }
  197. }
  198. }
  199. }
  200. // For operations that redirect endpoint & signing service based on
  201. // presence of OutpostId member. These operations will likely not
  202. // overlap with operations that perform ARN expansion.
  203. if (in_array($cmd->getName(), self::$outpostIdRedirectCmds)
  204. && !empty($cmd['OutpostId'])
  205. ) {
  206. $req = $req->withUri(
  207. $req->getUri()->withHost($this->generateOutpostIdHost())
  208. );
  209. $cmd['@context']['signing_service'] = 's3-outposts';
  210. }
  211. return $nextHandler($cmd, $req);
  212. }
  213. private function generateOutpostsArnHost(
  214. OutpostsArnInterface $arn,
  215. RequestInterface $req
  216. ) {
  217. if (!empty($this->config['use_arn_region']->isUseArnRegion())) {
  218. $region = $arn->getRegion();
  219. } else {
  220. $region = $this->region;
  221. }
  222. $suffix = $this->getPartitionSuffix($arn, $this->partitionProvider);
  223. return "s3-outposts.{$region}.{$suffix}";
  224. }
  225. private function generateOutpostIdHost()
  226. {
  227. $partition = $this->partitionProvider->getPartition(
  228. $this->region,
  229. $this->service->getEndpointPrefix()
  230. );
  231. $suffix = $partition->getDnsSuffix();
  232. return "s3-outposts.{$this->region}.{$suffix}";
  233. }
  234. private function validateBucketArn(ArnInterface $arn)
  235. {
  236. if ($arn instanceof BucketArnInterface) {
  237. return $this->validateArn($arn);
  238. }
  239. throw new InvalidArnException('Provided ARN was not a valid S3 bucket'
  240. . ' ARN.');
  241. }
  242. private function validateAccessPointArn(ArnInterface $arn)
  243. {
  244. if ($arn instanceof AccessPointArnInterface) {
  245. return $this->validateArn($arn);
  246. }
  247. throw new InvalidArnException('Provided ARN was not a valid S3 access'
  248. . ' point ARN.');
  249. }
  250. /**
  251. * Validates an ARN, returning a partition object corresponding to the ARN
  252. * if successful
  253. *
  254. * @param $arn
  255. * @return \Aws\Endpoint\Partition
  256. */
  257. private function validateArn(ArnInterface $arn)
  258. {
  259. // Dualstack is not supported with Outposts ARNs
  260. if ($arn instanceof OutpostsArnInterface
  261. && !empty($this->config['dual_stack'])
  262. ) {
  263. throw new UnresolvedEndpointException(
  264. 'Dualstack is currently not supported with S3 Outposts ARNs.'
  265. . ' Please disable dualstack or do not supply an Outposts ARN.');
  266. }
  267. // Get partitions for ARN and client region
  268. $arnPart = $this->partitionProvider->getPartitionByName(
  269. $arn->getPartition()
  270. );
  271. $clientPart = $this->partitionProvider->getPartition(
  272. $this->region,
  273. 's3'
  274. );
  275. // If client partition not found, try removing pseudo-region qualifiers
  276. if (!($clientPart->isRegionMatch($this->region, 's3'))) {
  277. $clientPart = $this->partitionProvider->getPartition(
  278. $this->stripPseudoRegions($this->region),
  279. 's3'
  280. );
  281. }
  282. // Verify that the partition matches for supplied partition and region
  283. if ($arn->getPartition() !== $clientPart->getName()) {
  284. throw new InvalidRegionException('The supplied ARN partition'
  285. . " does not match the client's partition.");
  286. }
  287. if ($clientPart->getName() !== $arnPart->getName()) {
  288. throw new InvalidRegionException('The corresponding partition'
  289. . ' for the supplied ARN region does not match the'
  290. . " client's partition.");
  291. }
  292. // Ensure ARN region matches client region unless
  293. // configured for using ARN region over client region
  294. $this->validateMatchingRegion($arn);
  295. // Ensure it is not resolved to fips pseudo-region for S3 Outposts
  296. $this->validateFipsNotUsedWithOutposts($arn);
  297. return $arnPart;
  298. }
  299. }