aws / aws-sdk-java-v2

The official AWS SDK for Java - Version 2
Apache License 2.0
2.12k stars 802 forks source link

UrlConnectionHttpClient returns null messages when S3.putObject returns 403 in some cases #5305

Open geerat opened 1 week ago

geerat commented 1 week ago

Describe the bug

When an v2 S3 client using the UrlConnectionHttpClient makes a putObject request with kms:sse that fails due to the caller not having kms:GenerateDataKey on the kms key being used to encrypt the file, the response is 403. However, the error message is null, when it should be something like User: XYZ is not authorized to perform: kms:GenerateDataKey on this resource because the resource does not exist in this Region, no resource-based policies allow access, or a resource-based policy explicitly denies access.

I have verified that a v2 S3 client using the ApacheHttpClient or AwsCrtHttpClient, or the v1 S3 client results in a non null error message with the expected message.

Expected Behavior

When calling putObject using kms:sse and specifying a kms key in the request with a key policy that does not give the caller permission to kms:GenerateDataKey then the request should fail with the error message:

User: {REDACTED PRINCIPAL} is not authorized to perform: kms:GenerateDataKey on this resource because the resource does not exist in this Region, no resource-based policies allow access, or a resource-based policy explicitly denies access (Service: S3, Status Code: 403, Request ID: 1DB63EJ6C0XM1Z1M, Extended Request ID: UrS2WQqsumheP9K8KKLCWgVlsnnSklK0MM9Idz2zaR/HeOw6WeWIlnpdCm+AjBv0y0h0umH9P/94Y4i5R+LW9Q==)

Current Behavior

When calling putObject using kms:sse and specifying a kms key in the request with a key policy that does not give the caller permission to kms:GenerateDataKey then the request fails with the following error message:

null (Service: S3, Status Code: 403, Request ID: 1NHWS3N3B2Q8KHFG, Extended Request ID: qNCefm0Z1GuLMVTUr2ew8OiKQeMyToU/Z8nBGe+zjjORC7JPauIE3tOg7MdxWPTPPxAGUQrPvJidHH8EHu+uqQ==)

Reproduction Steps

Below is a main method from a mvn project that will run with three different http clients, and the v1 s3 client.

package com.example;

import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.SSEAwsKeyManagementParams;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.crt.AwsCrtHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.ServerSideEncryption;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider;
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.http.apache.ApacheHttpClient;

public class App {

    private static final String bucketName = "insert your own bucket name";
    private static final String kmsKeyId = "insert your own kms key";
    private static final String role = "insert your role arn";
    private static final String key = "random/path.txt";

    public static void main(String[] args) throws IOException {
        v1Client();

        putObjectWithClient(getS3ClientWithHttpClient(ApacheHttpClient.create()));

        putObjectWithClient(getS3ClientWithHttpClient(UrlConnectionHttpClient.create()));

        putObjectWithClient(getS3ClientWithHttpClient(AwsCrtHttpClient.create()));
        System.exit(0);
    }

    private static StsClient getStsClient() {
        return StsClient.builder()
                .httpClientBuilder(UrlConnectionHttpClient.builder())
                .region(Region.AP_SOUTHEAST_2)
                .credentialsProvider(software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider.create())
                .build();
    }

    private static void v1Client() throws IOException {
        InputStream inputStream = generateFileContent();
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(inputStream.available());

        AmazonS3 s3clientv1 = AmazonS3ClientBuilder.standard()
                .withRegion("ap-southeast-2")
                .withCredentials(new ProfileCredentialsProvider())
                .build();

        try {
            SSEAwsKeyManagementParams kmsParams = new SSEAwsKeyManagementParams(kmsKeyId);
            PutObjectRequest request = new PutObjectRequest(bucketName, key, inputStream, metadata)
                    .withSSEAwsKeyManagementParams(kmsParams);
            s3clientv1.putObject(request);

            System.out.println("Object uploaded to S3 successfully, this means you made your key policy allow kms:GenerateDataKey!");
            System.exit(1);
        } catch (Exception e) {
            System.out.printf("v1 errorMsg: %s", e.getMessage());
        }
    }

    private static void putObjectWithClient(final S3Client s3Client) {
        final InputStream inputStream = generateFileContent();
        try {
            // Upload the object to S3 with SSE-KMS encryption
            var putObjectRequest = software.amazon.awssdk.services.s3.model.PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .serverSideEncryption(ServerSideEncryption.AWS_KMS)
                    .ssekmsKeyId(kmsKeyId)
                    .build();
            s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, inputStream.available()));

            System.out.println("Object uploaded to S3 successfully, this means you made your key policy allow kms:GenerateDataKey!");
            System.exit(1);
        } catch (Exception e) {
            System.out.println("ErrorMessage: \n" + e.getMessage());

        }
    }

    private static S3Client getS3ClientWithHttpClient(final SdkHttpClient sdkHttpClient) {
        final var assumeRoleRequest = AssumeRoleRequest.builder()
                .roleArn()
                .roleSessionName("whatever")
                .build();
        return S3Client.builder()
                .httpClient(sdkHttpClient)
                .credentialsProvider(
                        StsAssumeRoleCredentialsProvider.builder()
                                .stsClient(getStsClient())
                                .refreshRequest(assumeRoleRequest)
                                .build()
                )
                .region(Region.AP_SOUTHEAST_2)
                .build();
    }

    private static InputStream generateFileContent() {
        String content = "random file content for demo purposes 123432423";
        byte[] contentAsBytes = content.getBytes(StandardCharsets.UTF_8);
        return new ByteArrayInputStream(contentAsBytes);
    }
}

you will need the following dependencies:

 <!-- AWS SDK for Java v1 -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.539</version> 
        </dependency>

        <!-- AWS SDK for Java v2 -->
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
            <version>2.25.61</version>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>sts</artifactId>
            <version>2.25.61</version>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>url-connection-client</artifactId>
            <version>2.25.61</version>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>aws-crt-client</artifactId>
            <version>2.25.61</version>
        </dependency>

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>apache-client</artifactId>
            <version>2.25.61</version>
        </dependency>

Possible Solution

No response

Additional Information/Context

No response

AWS Java SDK version used

2.25.61

JDK version used

OpenJDK Runtime Environment Corretto-11.0.18.10.1 (build 11.0.18+10-LTS) OpenJDK 64-Bit Server VM Corretto-11.0.18.10.1 (build 11.0.18+10-LTS, mixed mode)

Operating System and version

Amazon Linux 2 x86_64, but I see this in multiple places

bhoradc commented 1 week ago

Hi @geerat,

Thank you for reporting the issue. The null message scenario for UrlConnectionHttpClient is reproducible.

We'll investigate and review it with the SDK team to take it further.

Regards, Chaitanya