Skip to content

Commit 56ad313

Browse files
committed
Create initial S3 endpoint resource/handler
Closes #8
1 parent ac858cb commit 56ad313

File tree

11 files changed

+214
-17
lines changed

11 files changed

+214
-17
lines changed

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
<dep.airlift.version>245</dep.airlift.version>
4949
<dep.aws-sdk.version>2.25.32</dep.aws-sdk.version>
50+
<dep.jersey.version>3.1.6</dep.jersey.version>
5051
</properties>
5152

5253
<dependencyManagement>
@@ -66,6 +67,12 @@
6667
<type>pom</type>
6768
<scope>import</scope>
6869
</dependency>
70+
71+
<dependency>
72+
<groupId>org.glassfish.jersey.core</groupId>
73+
<artifactId>jersey-server</artifactId>
74+
<version>${dep.jersey.version}</version>
75+
</dependency>
6976
</dependencies>
7077
</dependencyManagement>
7178

trino-s3-proxy/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@
6464
<artifactId>jakarta.ws.rs-api</artifactId>
6565
</dependency>
6666

67+
<dependency>
68+
<groupId>org.glassfish.jersey.core</groupId>
69+
<artifactId>jersey-server</artifactId>
70+
</dependency>
71+
6772
<dependency>
6873
<groupId>org.assertj</groupId>
6974
<artifactId>assertj-core</artifactId>

trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@
1515

1616
import static java.util.Objects.requireNonNull;
1717

18-
public record Credentials(String emulatedAccessKey, String emulatedSecretKey)
18+
public record Credentials(CredentialsEntry emulated)
1919
{
2020
public Credentials
2121
{
22-
requireNonNull(emulatedAccessKey, "emulatedAccessKey is null");
23-
requireNonNull(emulatedSecretKey, "emulatedSecretKey is null");
22+
requireNonNull(emulated, "emulated is null");
23+
}
24+
25+
public record CredentialsEntry(String accessKey, String secretKey)
26+
{
27+
public CredentialsEntry
28+
{
29+
requireNonNull(accessKey, "accessKey is null");
30+
requireNonNull(secretKey, "secretKey is null");
31+
}
2432
}
2533
}

trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@
1818
public interface CredentialsController
1919
{
2020
Optional<Credentials> credentials(String emulatedAccessKey);
21+
22+
void upsertCredentials(Credentials credentials);
2123
}

trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package io.trino.s3.proxy.server;
1515

16+
import com.google.common.base.Splitter;
1617
import com.google.inject.Inject;
1718
import io.trino.s3.proxy.server.minio.Signer;
1819
import io.trino.s3.proxy.server.minio.emulation.MinioRequest;
@@ -21,7 +22,9 @@
2122

2223
import java.security.InvalidKeyException;
2324
import java.security.NoSuchAlgorithmException;
25+
import java.util.List;
2426
import java.util.Map;
27+
import java.util.Optional;
2528

2629
import static java.util.Objects.requireNonNull;
2730

@@ -35,10 +38,58 @@ public SigningController(CredentialsController credentialsController)
3538
this.credentialsController = requireNonNull(credentialsController, "credentialsController is null");
3639
}
3740

38-
public Map<String, String> signedRequestHeaders(String method, MultivaluedMap<String, String> requestHeaders, String encodedPath, String encodedQuery, String region, String accessKey)
41+
public record Scope(String authorization, String accessKey, String region)
42+
{
43+
public Scope
44+
{
45+
authorization = requireNonNull(authorization, "accessKey is null");
46+
accessKey = requireNonNull(accessKey, "accessKey is null");
47+
region = requireNonNull(region, "region is null");
48+
}
49+
50+
public static Optional<Scope> fromHeaders(MultivaluedMap<String, String> requestHeaders)
51+
{
52+
String authorization = requestHeaders.getFirst("Authorization");
53+
if (authorization == null) {
54+
return Optional.empty();
55+
}
56+
57+
List<String> authorizationParts = Splitter.on(",").trimResults().splitToList(authorization);
58+
if (authorizationParts.isEmpty()) {
59+
return Optional.empty();
60+
}
61+
62+
String credential = authorizationParts.getFirst();
63+
List<String> credentialParts = Splitter.on("=").splitToList(credential);
64+
if (credentialParts.size() < 2) {
65+
return Optional.empty();
66+
}
67+
68+
String credentialValue = credentialParts.get(1);
69+
List<String> credentialValueParts = Splitter.on("/").splitToList(credentialValue);
70+
if (credentialValueParts.size() < 3) {
71+
return Optional.empty();
72+
}
73+
74+
String accessKey = credentialValueParts.getFirst();
75+
String region = credentialValueParts.get(2);
76+
return Optional.of(new Scope(authorization, accessKey, region));
77+
}
78+
}
79+
80+
public boolean validateRequest(String method, MultivaluedMap<String, String> requestHeaders, String encodedPath, String encodedQuery)
81+
{
82+
return Scope.fromHeaders(requestHeaders).map(scope -> {
83+
Map<String, String> signedRequestHeaders = signedRequestHeaders(scope, method, requestHeaders, encodedPath, encodedQuery);
84+
String requestAuthorization = signedRequestHeaders.get("Authorization");
85+
return scope.authorization.equals(requestAuthorization);
86+
}).orElse(false);
87+
}
88+
89+
public Map<String, String> signedRequestHeaders(Scope scope, String method, MultivaluedMap<String, String> requestHeaders, String encodedPath, String encodedQuery)
3990
{
4091
// TODO
41-
Credentials credentials = credentialsController.credentials(accessKey).orElseThrow();
92+
Credentials credentials = credentialsController.credentials(scope.accessKey).orElseThrow();
4293

4394
MinioUrl minioUrl = MinioUrl.build(encodedPath, encodedQuery);
4495
MinioRequest minioRequest = MinioRequest.build(requestHeaders, method, minioUrl);
@@ -47,7 +98,7 @@ public Map<String, String> signedRequestHeaders(String method, MultivaluedMap<St
4798
String sha256 = minioRequest.headerValue("x-amz-content-sha256").orElseThrow();
4899

49100
try {
50-
return Signer.signV4S3(minioRequest, region, accessKey, credentials.emulatedSecretKey(), sha256).headers();
101+
return Signer.signV4S3(minioRequest, scope.region, scope.accessKey, credentials.emulated().secretKey(), sha256).headers();
51102
}
52103
catch (NoSuchAlgorithmException | InvalidKeyException e) {
53104
// TODO

trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/TrinoS3ProxyServerModule.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import com.google.inject.Binder;
1717
import com.google.inject.Module;
18+
import com.google.inject.Scopes;
1819
import io.trino.s3.proxy.server.rest.TrinoS3ProxyResource;
1920

2021
import static io.airlift.jaxrs.JaxrsBinder.jaxrsBinder;
@@ -26,5 +27,7 @@ public class TrinoS3ProxyServerModule
2627
public void configure(Binder binder)
2728
{
2829
jaxrsBinder(binder).bind(TrinoS3ProxyResource.class);
30+
31+
binder.bind(SigningController.class).in(Scopes.SINGLETON);
2932
}
3033
}

trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/rest/TrinoS3ProxyResource.java

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,78 @@
1313
*/
1414
package io.trino.s3.proxy.server.rest;
1515

16+
import com.google.inject.Inject;
17+
import io.trino.s3.proxy.server.SigningController;
18+
import jakarta.ws.rs.Consumes;
1619
import jakarta.ws.rs.GET;
20+
import jakarta.ws.rs.HEAD;
1721
import jakarta.ws.rs.Path;
22+
import jakarta.ws.rs.PathParam;
1823
import jakarta.ws.rs.Produces;
24+
import jakarta.ws.rs.WebApplicationException;
25+
import jakarta.ws.rs.core.Context;
1926
import jakarta.ws.rs.core.MediaType;
27+
import jakarta.ws.rs.core.Response;
28+
import org.glassfish.jersey.server.ContainerRequest;
2029

21-
@Path(TrinoS3ProxyRestConstants.BASE_PATH)
30+
import static com.google.common.base.MoreObjects.firstNonNull;
31+
import static java.util.Objects.requireNonNull;
32+
33+
@Path(TrinoS3ProxyRestConstants.S3_PATH)
2234
public class TrinoS3ProxyResource
2335
{
36+
private final SigningController signingController;
37+
38+
@Inject
39+
public TrinoS3ProxyResource(SigningController signingController)
40+
{
41+
this.signingController = requireNonNull(signingController, "signingController is null");
42+
}
43+
2444
@GET
25-
@Path("hello")
45+
@Consumes(MediaType.APPLICATION_JSON)
46+
@Produces(MediaType.APPLICATION_JSON)
47+
public Response s3Get(@Context ContainerRequest request)
48+
{
49+
return s3Get(request, "");
50+
}
51+
52+
@GET
53+
@Path("{bucket:.*}")
54+
@Consumes(MediaType.APPLICATION_JSON)
55+
@Produces(MediaType.APPLICATION_JSON)
56+
public Response s3Get(@Context ContainerRequest request, @PathParam("bucket") String bucket)
57+
{
58+
validateRequest(request);
59+
return Response.ok().build();
60+
}
61+
62+
@HEAD
63+
@Consumes(MediaType.APPLICATION_JSON)
2664
@Produces(MediaType.APPLICATION_JSON)
27-
public String hello()
65+
public Response s3Head(@Context ContainerRequest request)
2866
{
29-
return "hello";
67+
return s3Head(request, "");
68+
}
69+
70+
@HEAD
71+
@Path("{bucket}/{remainingPath:.*}")
72+
@Consumes(MediaType.APPLICATION_JSON)
73+
@Produces(MediaType.APPLICATION_JSON)
74+
public Response s3Head(@Context ContainerRequest request, @PathParam("bucket") String bucket)
75+
{
76+
validateRequest(request);
77+
return Response.ok().build();
78+
}
79+
80+
private void validateRequest(ContainerRequest request)
81+
{
82+
String encodedPath = "/" + firstNonNull(request.getPath(false), "");
83+
String encodedQuery = firstNonNull(request.getUriInfo().getRequestUri().getRawQuery(), "");
84+
85+
if (!signingController.validateRequest(request.getMethod(), request.getRequestHeaders(), encodedPath, encodedQuery)) {
86+
// TODO logging, etc.
87+
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
88+
}
3089
}
3190
}

trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/rest/TrinoS3ProxyRestConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ public final class TrinoS3ProxyRestConstants
1818
private TrinoS3ProxyRestConstants() {}
1919

2020
public static final String BASE_PATH = "/api/v1/s3Proxy/";
21+
public static final String S3_PATH = BASE_PATH + "s3";
2122
}

trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
*/
1414
package io.trino.s3.proxy.server;
1515

16+
import io.trino.s3.proxy.server.Credentials.CredentialsEntry;
17+
import io.trino.s3.proxy.server.SigningController.Scope;
1618
import jakarta.ws.rs.core.MultivaluedHashMap;
1719
import jakarta.ws.rs.core.MultivaluedMap;
1820
import org.junit.jupiter.api.Test;
@@ -24,12 +26,26 @@
2426

2527
public class TestSigningController
2628
{
27-
private static final Credentials CREDENTIALS = new Credentials("THIS_IS_AN_ACCESS_KEY", "THIS_IS_A_SECRET_KEY");
29+
private static final Credentials CREDENTIALS = new Credentials(new CredentialsEntry("THIS_IS_AN_ACCESS_KEY", "THIS_IS_A_SECRET_KEY"));
30+
31+
private final CredentialsController credentialsController = new CredentialsController()
32+
{
33+
@Override
34+
public Optional<Credentials> credentials(String emulatedAccessKey)
35+
{
36+
return Optional.of(CREDENTIALS);
37+
}
38+
39+
@Override
40+
public void upsertCredentials(Credentials credentials)
41+
{
42+
throw new UnsupportedOperationException();
43+
}
44+
};
2845

2946
@Test
3047
public void testRootLs()
3148
{
32-
CredentialsController credentialsController = accessKey -> Optional.of(CREDENTIALS);
3349
SigningController signingController = new SigningController(credentialsController);
3450

3551
// values discovered from an AWS CLI request sent to a dummy local HTTP server
@@ -41,15 +57,14 @@ public void testRootLs()
4157
requestHeaders.putSingle("User-Agent", "aws-cli/2.15.16 Python/3.11.7 Darwin/22.6.0 source/x86_64 prompt/off command/s3.ls");
4258
requestHeaders.putSingle("Accept-Encoding", "identity");
4359

44-
Map<String, String> signedHeaders = signingController.signedRequestHeaders("GET", requestHeaders, "/", "", "us-east-1", "THIS_IS_AN_ACCESS_KEY");
60+
Map<String, String> signedHeaders = signingController.signedRequestHeaders(new Scope("dummy", "THIS_IS_AN_ACCESS_KEY", "us-east-1"), "GET", requestHeaders, "/", "");
4561

4662
assertThat(signedHeaders).contains(Map.entry("Authorization", "AWS4-HMAC-SHA256 Credential=THIS_IS_AN_ACCESS_KEY/20240516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=9a19c251bf4e1533174e80da59fa57c65b3149b611ec9a4104f6944767c25704"));
4763
}
4864

4965
@Test
5066
public void testBucketLs()
5167
{
52-
CredentialsController credentialsController = accessKey -> Optional.of(CREDENTIALS);
5368
SigningController signingController = new SigningController(credentialsController);
5469

5570
// values discovered from an AWS CLI request sent to a dummy local HTTP server
@@ -61,7 +76,7 @@ public void testBucketLs()
6176
requestHeaders.putSingle("User-Agent", "aws-cli/2.15.16 Python/3.11.7 Darwin/22.6.0 source/x86_64 prompt/off command/s3.ls");
6277
requestHeaders.putSingle("Accept-Encoding", "identity");
6378

64-
Map<String, String> signedHeaders = signingController.signedRequestHeaders("GET", requestHeaders, "/mybucket", "list-type=2&prefix=foo%2Fbar&delimiter=%2F&encoding-type=url", "us-east-1", "THIS_IS_AN_ACCESS_KEY");
79+
Map<String, String> signedHeaders = signingController.signedRequestHeaders(new Scope("dummy", "THIS_IS_AN_ACCESS_KEY", "us-east-1"), "GET", requestHeaders, "/mybucket", "list-type=2&prefix=foo%2Fbar&delimiter=%2F&encoding-type=url");
6580

6681
assertThat(signedHeaders).contains(Map.entry("Authorization", "AWS4-HMAC-SHA256 Credential=THIS_IS_AN_ACCESS_KEY/20240516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=222d7b7fcd4d5560c944e8fecd9424ee3915d131c3ad9e000d65db93e87946c4"));
6782
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package io.trino.s3.proxy.server;
15+
16+
import java.util.Map;
17+
import java.util.Optional;
18+
import java.util.concurrent.ConcurrentHashMap;
19+
20+
public class TestingCredentialsController
21+
implements CredentialsController
22+
{
23+
private final Map<String, Credentials> credentials = new ConcurrentHashMap<>();
24+
25+
@Override
26+
public Optional<Credentials> credentials(String emulatedAccessKey)
27+
{
28+
return Optional.ofNullable(credentials.get(emulatedAccessKey));
29+
}
30+
31+
@Override
32+
public void upsertCredentials(Credentials credentials)
33+
{
34+
this.credentials.put(credentials.emulated().accessKey(), credentials);
35+
}
36+
}

0 commit comments

Comments
 (0)