EndpointArnMiddleware.php 13 KB

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