diff --git a/manifest.json b/manifest.json index 712fcb508..2ef409857 100644 --- a/manifest.json +++ b/manifest.json @@ -558,6 +558,7 @@ "PutObject", "PutObjectAcl", "PutObjectTagging", + "PutPublicAccessBlock", "UploadPart", "UploadPartCopy" ] diff --git a/src/Service/S3/composer.json b/src/Service/S3/composer.json index 8b5e5c4c0..4989a3764 100644 --- a/src/Service/S3/composer.json +++ b/src/Service/S3/composer.json @@ -16,7 +16,7 @@ "ext-filter": "*", "ext-hash": "*", "ext-simplexml": "*", - "async-aws/core": "^1.22" + "async-aws/core": "^1.9" }, "autoload": { "psr-4": { diff --git a/src/Service/S3/src/Input/PutPublicAccessBlockRequest.php b/src/Service/S3/src/Input/PutPublicAccessBlockRequest.php new file mode 100644 index 000000000..bc02d7789 --- /dev/null +++ b/src/Service/S3/src/Input/PutPublicAccessBlockRequest.php @@ -0,0 +1,221 @@ +bucket = $input['Bucket'] ?? null; + $this->contentMd5 = $input['ContentMD5'] ?? null; + $this->checksumAlgorithm = $input['ChecksumAlgorithm'] ?? null; + $this->publicAccessBlockConfiguration = isset($input['PublicAccessBlockConfiguration']) ? PublicAccessBlockConfiguration::create($input['PublicAccessBlockConfiguration']) : null; + $this->expectedBucketOwner = $input['ExpectedBucketOwner'] ?? null; + parent::__construct($input); + } + + /** + * @param array{ + * Bucket?: string, + * ContentMD5?: string|null, + * ChecksumAlgorithm?: ChecksumAlgorithm::*|null, + * PublicAccessBlockConfiguration?: PublicAccessBlockConfiguration|array, + * ExpectedBucketOwner?: string|null, + * '@region'?: string|null, + * }|PutPublicAccessBlockRequest $input + */ + public static function create($input): self + { + return $input instanceof self ? $input : new self($input); + } + + public function getBucket(): ?string + { + return $this->bucket; + } + + /** + * @return ChecksumAlgorithm::*|null + */ + public function getChecksumAlgorithm(): ?string + { + return $this->checksumAlgorithm; + } + + public function getContentMd5(): ?string + { + return $this->contentMd5; + } + + public function getExpectedBucketOwner(): ?string + { + return $this->expectedBucketOwner; + } + + public function getPublicAccessBlockConfiguration(): ?PublicAccessBlockConfiguration + { + return $this->publicAccessBlockConfiguration; + } + + /** + * @internal + */ + public function request(): Request + { + // Prepare headers + $headers = ['content-type' => 'application/xml']; + if (null !== $this->contentMd5) { + $headers['Content-MD5'] = $this->contentMd5; + } + if (null !== $this->checksumAlgorithm) { + if (!ChecksumAlgorithm::exists($this->checksumAlgorithm)) { + throw new InvalidArgument(\sprintf('Invalid parameter "ChecksumAlgorithm" for "%s". The value "%s" is not a valid "ChecksumAlgorithm".', __CLASS__, $this->checksumAlgorithm)); + } + $headers['x-amz-sdk-checksum-algorithm'] = $this->checksumAlgorithm; + } + if (null !== $this->expectedBucketOwner) { + $headers['x-amz-expected-bucket-owner'] = $this->expectedBucketOwner; + } + + // Prepare query + $query = []; + + // Prepare URI + $uri = []; + if (null === $v = $this->bucket) { + throw new InvalidArgument(\sprintf('Missing parameter "Bucket" for "%s". The value cannot be null.', __CLASS__)); + } + $uri['Bucket'] = $v; + $uriString = '/' . rawurlencode($uri['Bucket']) . '?publicAccessBlock'; + + // Prepare Body + + $document = new \DOMDocument('1.0', 'UTF-8'); + $document->formatOutput = false; + $this->requestBody($document, $document); + $body = $document->hasChildNodes() ? $document->saveXML() : ''; + + // Return the Request + return new Request('PUT', $uriString, $query, $headers, StreamFactory::create($body)); + } + + public function setBucket(?string $value): self + { + $this->bucket = $value; + + return $this; + } + + /** + * @param ChecksumAlgorithm::*|null $value + */ + public function setChecksumAlgorithm(?string $value): self + { + $this->checksumAlgorithm = $value; + + return $this; + } + + public function setContentMd5(?string $value): self + { + $this->contentMd5 = $value; + + return $this; + } + + public function setExpectedBucketOwner(?string $value): self + { + $this->expectedBucketOwner = $value; + + return $this; + } + + public function setPublicAccessBlockConfiguration(?PublicAccessBlockConfiguration $value): self + { + $this->publicAccessBlockConfiguration = $value; + + return $this; + } + + private function requestBody(\DOMNode $node, \DOMDocument $document): void + { + if (null === $v = $this->publicAccessBlockConfiguration) { + throw new InvalidArgument(\sprintf('Missing parameter "PublicAccessBlockConfiguration" for "%s". The value cannot be null.', __CLASS__)); + } + + $node->appendChild($child = $document->createElement('PublicAccessBlockConfiguration')); + $child->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); + $v->requestBody($child, $document); + } +} diff --git a/src/Service/S3/src/S3Client.php b/src/Service/S3/src/S3Client.php index 7b4e1154f..6b285da46 100644 --- a/src/Service/S3/src/S3Client.php +++ b/src/Service/S3/src/S3Client.php @@ -62,6 +62,7 @@ use AsyncAws\S3\Input\PutObjectAclRequest; use AsyncAws\S3\Input\PutObjectRequest; use AsyncAws\S3\Input\PutObjectTaggingRequest; +use AsyncAws\S3\Input\PutPublicAccessBlockRequest; use AsyncAws\S3\Input\UploadPartCopyRequest; use AsyncAws\S3\Input\UploadPartRequest; use AsyncAws\S3\Result\AbortMultipartUploadOutput; @@ -101,6 +102,7 @@ use AsyncAws\S3\ValueObject\MultipartUpload; use AsyncAws\S3\ValueObject\NotificationConfiguration; use AsyncAws\S3\ValueObject\Part; +use AsyncAws\S3\ValueObject\PublicAccessBlockConfiguration; use AsyncAws\S3\ValueObject\Tagging; class S3Client extends AbstractApi @@ -2891,6 +2893,54 @@ public function putObjectTagging($input): PutObjectTaggingOutput return new PutObjectTaggingOutput($response); } + /** + * > This operation is not supported for directory buckets. + * + * Creates or modifies the `PublicAccessBlock` configuration for an Amazon S3 bucket. To use this operation, you must + * have the `s3:PutBucketPublicAccessBlock` permission. For more information about Amazon S3 permissions, see Specifying + * Permissions in a Policy [^1]. + * + * ! When Amazon S3 evaluates the `PublicAccessBlock` configuration for a bucket or an object, it checks the + * ! `PublicAccessBlock` configuration for both the bucket (or the bucket that contains the object) and the bucket + * ! owner's account. If the `PublicAccessBlock` configurations are different between the bucket and the account, Amazon + * ! S3 uses the most restrictive combination of the bucket-level and account-level settings. + * + * For more information about when Amazon S3 considers a bucket or an object public, see The Meaning of "Public" [^2]. + * + * The following operations are related to `PutPublicAccessBlock`: + * + * - GetPublicAccessBlock [^3] + * - DeletePublicAccessBlock [^4] + * - GetBucketPolicyStatus [^5] + * - Using Amazon S3 Block Public Access [^6] + * + * [^1]: https://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html + * [^2]: https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-policy-status + * [^3]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetPublicAccessBlock.html + * [^4]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeletePublicAccessBlock.html + * [^5]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html + * [^6]: https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutPublicAccessBlock.html + * @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putpublicaccessblock + * + * @param array{ + * Bucket: string, + * ContentMD5?: string|null, + * ChecksumAlgorithm?: ChecksumAlgorithm::*|null, + * PublicAccessBlockConfiguration: PublicAccessBlockConfiguration|array, + * ExpectedBucketOwner?: string|null, + * '@region'?: string|null, + * }|PutPublicAccessBlockRequest $input + */ + public function putPublicAccessBlock($input): Result + { + $input = PutPublicAccessBlockRequest::create($input); + $response = $this->getResponse($input->request(), new RequestContext(['operation' => 'PutPublicAccessBlock', 'region' => $input->getRegion()])); + + return new Result($response); + } + /** * Uploads a part in a multipart upload. * diff --git a/src/Service/S3/src/ValueObject/PublicAccessBlockConfiguration.php b/src/Service/S3/src/ValueObject/PublicAccessBlockConfiguration.php new file mode 100644 index 000000000..f241bf130 --- /dev/null +++ b/src/Service/S3/src/ValueObject/PublicAccessBlockConfiguration.php @@ -0,0 +1,128 @@ +blockPublicAcls = $input['BlockPublicAcls'] ?? null; + $this->ignorePublicAcls = $input['IgnorePublicAcls'] ?? null; + $this->blockPublicPolicy = $input['BlockPublicPolicy'] ?? null; + $this->restrictPublicBuckets = $input['RestrictPublicBuckets'] ?? null; + } + + /** + * @param array{ + * BlockPublicAcls?: bool|null, + * IgnorePublicAcls?: bool|null, + * BlockPublicPolicy?: bool|null, + * RestrictPublicBuckets?: bool|null, + * }|PublicAccessBlockConfiguration $input + */ + public static function create($input): self + { + return $input instanceof self ? $input : new self($input); + } + + public function getBlockPublicAcls(): ?bool + { + return $this->blockPublicAcls; + } + + public function getBlockPublicPolicy(): ?bool + { + return $this->blockPublicPolicy; + } + + public function getIgnorePublicAcls(): ?bool + { + return $this->ignorePublicAcls; + } + + public function getRestrictPublicBuckets(): ?bool + { + return $this->restrictPublicBuckets; + } + + /** + * @internal + */ + public function requestBody(\DOMElement $node, \DOMDocument $document): void + { + if (null !== $v = $this->blockPublicAcls) { + $node->appendChild($document->createElement('BlockPublicAcls', $v ? 'true' : 'false')); + } + if (null !== $v = $this->ignorePublicAcls) { + $node->appendChild($document->createElement('IgnorePublicAcls', $v ? 'true' : 'false')); + } + if (null !== $v = $this->blockPublicPolicy) { + $node->appendChild($document->createElement('BlockPublicPolicy', $v ? 'true' : 'false')); + } + if (null !== $v = $this->restrictPublicBuckets) { + $node->appendChild($document->createElement('RestrictPublicBuckets', $v ? 'true' : 'false')); + } + } +} diff --git a/src/Service/S3/tests/Integration/S3ClientTest.php b/src/Service/S3/tests/Integration/S3ClientTest.php index 8df27d620..0e9b3d9b6 100644 --- a/src/Service/S3/tests/Integration/S3ClientTest.php +++ b/src/Service/S3/tests/Integration/S3ClientTest.php @@ -36,6 +36,7 @@ use AsyncAws\S3\Input\PutObjectAclRequest; use AsyncAws\S3\Input\PutObjectRequest; use AsyncAws\S3\Input\PutObjectTaggingRequest; +use AsyncAws\S3\Input\PutPublicAccessBlockRequest; use AsyncAws\S3\Input\UploadPartCopyRequest; use AsyncAws\S3\Input\UploadPartRequest; use AsyncAws\S3\Result\PutObjectOutput; @@ -55,6 +56,7 @@ use AsyncAws\S3\ValueObject\NotificationConfiguration; use AsyncAws\S3\ValueObject\NotificationConfigurationFilter; use AsyncAws\S3\ValueObject\Owner; +use AsyncAws\S3\ValueObject\PublicAccessBlockConfiguration; use AsyncAws\S3\ValueObject\QueueConfiguration; use AsyncAws\S3\ValueObject\S3KeyFilter; use AsyncAws\S3\ValueObject\Tag; @@ -918,6 +920,27 @@ public function testPutObjectTagging(): void self::assertSame(200, $result->info()['status']); } + public function testPutPublicAccessBlock(): void + { + $client = $this->getClient(); + + $input = new PutPublicAccessBlockRequest([ + 'Bucket' => 'change me', + 'ContentMD5' => 'change me', + 'ChecksumAlgorithm' => 'change me', + 'PublicAccessBlockConfiguration' => new PublicAccessBlockConfiguration([ + 'BlockPublicAcls' => false, + 'IgnorePublicAcls' => false, + 'BlockPublicPolicy' => false, + 'RestrictPublicBuckets' => false, + ]), + 'ExpectedBucketOwner' => 'change me', + ]); + $result = $client->putPublicAccessBlock($input); + + $result->resolve(); + } + public function testUploadFromClosure() { $parts = ['some ', 'content']; diff --git a/src/Service/S3/tests/Unit/Input/PutPublicAccessBlockRequestTest.php b/src/Service/S3/tests/Unit/Input/PutPublicAccessBlockRequestTest.php new file mode 100644 index 000000000..76784ff3d --- /dev/null +++ b/src/Service/S3/tests/Unit/Input/PutPublicAccessBlockRequestTest.php @@ -0,0 +1,38 @@ + 'change me', + 'ContentMD5' => 'change me', + 'ChecksumAlgorithm' => 'change me', + 'PublicAccessBlockConfiguration' => new PublicAccessBlockConfiguration([ + 'BlockPublicAcls' => false, + 'IgnorePublicAcls' => false, + 'BlockPublicPolicy' => false, + 'RestrictPublicBuckets' => false, + ]), + 'ExpectedBucketOwner' => 'change me', + ]); + + // see https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutPublicAccessBlock.html + $expected = ' + PUT / HTTP/1.0 + Content-Type: application/xml + + it + '; + + self::assertRequestEqualsHttpRequest($expected, $input->request()); + } +} diff --git a/src/Service/S3/tests/Unit/S3ClientTest.php b/src/Service/S3/tests/Unit/S3ClientTest.php index 82135fac3..e8fc4764a 100644 --- a/src/Service/S3/tests/Unit/S3ClientTest.php +++ b/src/Service/S3/tests/Unit/S3ClientTest.php @@ -33,6 +33,7 @@ use AsyncAws\S3\Input\PutObjectAclRequest; use AsyncAws\S3\Input\PutObjectRequest; use AsyncAws\S3\Input\PutObjectTaggingRequest; +use AsyncAws\S3\Input\PutPublicAccessBlockRequest; use AsyncAws\S3\Input\UploadPartCopyRequest; use AsyncAws\S3\Input\UploadPartRequest; use AsyncAws\S3\Result\AbortMultipartUploadOutput; @@ -67,6 +68,7 @@ use AsyncAws\S3\ValueObject\NotificationConfiguration; use AsyncAws\S3\ValueObject\NotificationConfigurationFilter; use AsyncAws\S3\ValueObject\ObjectIdentifier; +use AsyncAws\S3\ValueObject\PublicAccessBlockConfiguration; use AsyncAws\S3\ValueObject\S3KeyFilter; use AsyncAws\S3\ValueObject\Tag; use AsyncAws\S3\ValueObject\Tagging; @@ -542,6 +544,22 @@ public function testPutObjectTagging(): void self::assertFalse($result->info()['resolved']); } + public function testPutPublicAccessBlock(): void + { + $client = new S3Client([], new NullProvider(), new MockHttpClient()); + + $input = new PutPublicAccessBlockRequest([ + 'Bucket' => 'change me', + + 'PublicAccessBlockConfiguration' => new PublicAccessBlockConfiguration([ + ]), + ]); + $result = $client->putPublicAccessBlock($input); + + self::assertInstanceOf(Result::class, $result); + self::assertFalse($result->info()['resolved']); + } + public function testUploadPart(): void { $client = new S3Client([], new NullProvider(), new MockHttpClient());