Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Serverless OpenSearch signing #90

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
### 2.2.1 (Next)

* [#90](https://github.com/acm19/aws-request-signing-apache-interceptor/pull/90): Add support for OpenSearch Serverless - [@acm19](https://github.com/acm19).

### 2.2.0 (2022/10/02)

* [#80](https://github.com/acm19/aws-request-signing-apache-interceptor/pull/80): Add support for Apache client v5 - [@acm19](https://github.com/acm19).
Expand Down
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ run_sample:
-Dexec.mainClass="io.github.acm19.aws.interceptor.test.AmazonOpenSearchServiceSample" \
-Dexec.args="--endpoint=$(ENDPOINT) --region=$(REGION)"

.PHONY: run_serverless_sample
.SILENT: run_serverless_sample
run_serverless_sample:
mvn test-compile exec:java \
-Dexec.classpathScope=test \
-Dexec.mainClass="io.github.acm19.aws.interceptor.test.AmazonOpenSearchServerlessSample" \
-Dexec.args="--endpoint=$(ENDPOINT) --region=$(REGION)"

.PHONY: run_v5_sample
.SILENT: run_v5_sample
run_v5_sample:
Expand All @@ -22,6 +30,14 @@ run_v5_sample:
-Dexec.mainClass="io.github.acm19.aws.interceptorv5.test.AmazonOpenSearchServiceSample" \
-Dexec.args="--endpoint=$(ENDPOINT) --region=$(REGION)"

.PHONY: run_v5_serverless_sample
.SILENT: run_v5_serverless_sample
run_v5_serverless_sample:
mvn test-compile exec:java \
-Dexec.classpathScope=test \
-Dexec.mainClass="io.github.acm19.aws.interceptorv5.test.AmazonOpenSearchServerlessSample" \
-Dexec.args="--endpoint=$(ENDPOINT) --region=$(REGION)"

debug_v5_sample:
mvn exec:exec -Dexec.executable="java" -Dexec.classpathScope=test \
-Dexec.args="-classpath %classpath -Xdebug \
Expand Down
1 change: 0 additions & 1 deletion checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@

<!-- See https://checkstyle.org/config_misc.html -->
<module name="ArrayTypeStyle"/>
<module name="FinalParameters"/>
<module name="TodoComment"/>
<module name="UpperEll"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
*/
public final class AwsRequestSigningApacheInterceptor implements HttpRequestInterceptor {
private final RequestSigner signer;
private final boolean isServerlessOpenSearch;

/**
* Creates an {@code AwsRequestSigningApacheInterceptor} with the
Expand All @@ -51,12 +52,12 @@ public final class AwsRequestSigningApacheInterceptor implements HttpRequestInte
* @param awsCredentialsProvider source of AWS credentials for signing
* @param region signing region
*/
public AwsRequestSigningApacheInterceptor(
final String service,
final Signer signer,
final AwsCredentialsProvider awsCredentialsProvider,
final Region region) {
public AwsRequestSigningApacheInterceptor(String service,
Signer signer,
AwsCredentialsProvider awsCredentialsProvider,
Region region) {
this.signer = new RequestSigner(service, signer, awsCredentialsProvider, region);
this.isServerlessOpenSearch = "aoss".equals(service);
}

/**
Expand All @@ -69,19 +70,18 @@ public AwsRequestSigningApacheInterceptor(
* @param awsCredentialsProvider source of AWS credentials for signing
* @param region signing region
*/
public AwsRequestSigningApacheInterceptor(
final String service,
final Signer signer,
final AwsCredentialsProvider awsCredentialsProvider,
final String region) {
public AwsRequestSigningApacheInterceptor(String service,
Signer signer,
AwsCredentialsProvider awsCredentialsProvider,
String region) {
this(service, signer, awsCredentialsProvider, Region.of(region));
}

/**
* {@inheritDoc}
*/
@Override
public void process(final HttpRequest request, final HttpContext context)
public void process(HttpRequest request, HttpContext context)
throws HttpException, IOException {
URI requestUri = RequestSigner.buildUri(context, request.getRequestLine().getUri());

Expand All @@ -101,8 +101,15 @@ public void process(final HttpRequest request, final HttpContext context)
requestBuilder.headers(headerArrayToMap(request.getAllHeaders()));
SdkHttpFullRequest signedRequest = signer.signRequest(requestBuilder.build());

// copy everything back
request.setHeaders(mapToHeaderArray(signedRequest.headers()));
if (!isServerlessOpenSearch) {
// copy everything back
request.setHeaders(mapToHeaderArray(signedRequest.headers()));
} else {
// copy everything back, don't override headers as not all of them were used for signing the request
for (Header header : mapToHeaderArray(signedRequest.headers())) {
request.setHeader(header);
}
}

if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest httpEntityEnclosingRequest = (HttpEntityEnclosingRequest) request;
Expand All @@ -117,7 +124,7 @@ public void process(final HttpRequest request, final HttpContext context)
}
}

private static Map<String, List<String>> headerArrayToMap(final Header[] headers) {
private Map<String, List<String>> headerArrayToMap(Header[] headers) {
Map<String, List<String>> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Header header : headers) {
if (!skipHeader(header)) {
Expand All @@ -129,13 +136,28 @@ private static Map<String, List<String>> headerArrayToMap(final Header[] headers
return headersMap;
}

private static boolean skipHeader(final Header header) {
/**
* Ignores {@code Host} headers and {@code Content-Length} as long as it either
* contains the value {@code 0} or the service to be signed is Serverless
* OpenSearch. Details in AWS documentation below.
*
* AWS documentation:
* <pre><a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-clients.html
* #serverless-signing">
* Signing requests to OpenSearch Serverless
* </a></pre>
*
* @param header the header to evaluate
* @return {@code true} if header must be ignored {@code false} otherwise
*/
private boolean skipHeader(Header header) {
// Strip for Content-Length: 0 and Serverless
return (HTTP.CONTENT_LEN.equalsIgnoreCase(header.getName())
&& "0".equals(header.getValue())) // Strip Content-Length: 0
&& (("0".equals(header.getValue())) || isServerlessOpenSearch))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something doesn't add up here for the managed service. Maybe we didn't need Content-Length for it if we included x-amz-content-sha256: UNSIGNED-PAYLOAD?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow you, if you don't want to sign the payload, why using the interceptor? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signing headers?

With TLS signing more or less data (body vs. headers) shouldn't really matter, as long as something is signed the server can ensure that the payload came from who claimed to send it.

|| HTTP.TARGET_HOST.equalsIgnoreCase(header.getName()); // Host comes from endpoint
}

private static Header[] mapToHeaderArray(final Map<String, List<String>> mapHeaders) {
private static Header[] mapToHeaderArray(Map<String, List<String>> mapHeaders) {
Header[] headers = new Header[mapHeaders.size()];
int i = 0;
for (Map.Entry<String, List<String>> headerEntry : mapHeaders.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
*/
public final class AwsRequestSigningApacheV5Interceptor implements HttpRequestInterceptor {
private final RequestSigner signer;
private final boolean isServerlessOpenSearch;

/**
* Creates an {@code AwsRequestSigningApacheInterceptor} with the
Expand All @@ -54,19 +55,19 @@ public final class AwsRequestSigningApacheV5Interceptor implements HttpRequestIn
* @param awsCredentialsProvider source of AWS credentials for signing
* @param region signing region
*/
public AwsRequestSigningApacheV5Interceptor(
final String service,
final Signer signer,
final AwsCredentialsProvider awsCredentialsProvider,
final Region region) {
public AwsRequestSigningApacheV5Interceptor(String service,
Signer signer,
AwsCredentialsProvider awsCredentialsProvider,
Region region) {
this.signer = new RequestSigner(service, signer, awsCredentialsProvider, region);
this.isServerlessOpenSearch = "aoss".equals(service);
}

/**
* {@inheritDoc}
*/
@Override
public void process(final HttpRequest request, final EntityDetails entityDetails, final HttpContext context)
public void process(HttpRequest request, EntityDetails entityDetails, HttpContext context)
throws HttpException, IOException {
// copy Apache HttpRequest to AWS request
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
Expand All @@ -85,8 +86,15 @@ public void process(final HttpRequest request, final EntityDetails entityDetails
requestBuilder.headers(headerArrayToMap(request.getHeaders()));
SdkHttpFullRequest signedRequest = signer.signRequest(requestBuilder.build());

// copy everything back
request.setHeaders(mapToHeaderArray(signedRequest.headers()));
if (!isServerlessOpenSearch) {
// copy everything back
request.setHeaders(mapToHeaderArray(signedRequest.headers()));
} else {
// copy everything back, don't override headers as not all of them were used for signing the request
for (Header header : mapToHeaderArray(signedRequest.headers())) {
request.setHeader(header);
}
}

if (request instanceof ClassicHttpRequest) {
ClassicHttpRequest httpEntityEnclosingRequest = (ClassicHttpRequest) request;
Expand All @@ -100,15 +108,15 @@ public void process(final HttpRequest request, final EntityDetails entityDetails
}
}

private static URI buildUri(final HttpRequest request) throws IOException {
private static URI buildUri(HttpRequest request) throws IOException {
try {
return request.getUri();
} catch (URISyntaxException ex) {
throw new IOException("Invalid URI", ex);
}
}

private static Map<String, List<String>> headerArrayToMap(final Header[] headers) {
private Map<String, List<String>> headerArrayToMap(Header[] headers) {
Map<String, List<String>> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Header header : headers) {
if (!skipHeader(header)) {
Expand All @@ -120,9 +128,24 @@ private static Map<String, List<String>> headerArrayToMap(final Header[] headers
return headersMap;
}

private static boolean skipHeader(final Header header) {
/**
* Ignores {@code Host} headers and {@code Content-Length} as long as it either
* contains the value {@code 0} or the service to be signed is Serverless
* OpenSearch. Details in AWS documentation below.
*
* AWS documentation:
* <pre><a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-clients.html
* #serverless-signing">
* Signing requests to OpenSearch Serverless
* </a></pre>
*
* @param header the header to evaluate
* @return {@code true} if header must be ignored {@code false} otherwise
*/
private boolean skipHeader(Header header) {
// Strip for Content-Length: 0 and Serverless
return (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(header.getName())
&& "0".equals(header.getValue())) // Strip Content-Length: 0
&& (("0".equals(header.getValue())) || isServerlessOpenSearch))
|| HttpHeaders.HOST.equalsIgnoreCase(header.getName()); // Host comes from endpoint
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ class RequestSigner {
* @param awsCredentialsProvider
* @param region
*/
RequestSigner(final String service,
final Signer signer,
final AwsCredentialsProvider awsCredentialsProvider,
final Region region) {
RequestSigner(String service,
Signer signer,
AwsCredentialsProvider awsCredentialsProvider,
Region region) {
this.service = service;
this.signer = signer;
this.awsCredentialsProvider = awsCredentialsProvider;
Expand All @@ -67,7 +67,7 @@ class RequestSigner {
* @return signed request
* @see Signer#sign
*/
SdkHttpFullRequest signRequest(final SdkHttpFullRequest request) {
SdkHttpFullRequest signRequest(SdkHttpFullRequest request) {
ExecutionAttributes attributes = new ExecutionAttributes();
attributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS,
awsCredentialsProvider.resolveCredentials());
Expand All @@ -86,7 +86,7 @@ SdkHttpFullRequest signRequest(final SdkHttpFullRequest request) {
* @return an {@link URI} from an HTTP context
* @throws IOException if the {@code uri} syntax is invalid
*/
static URI buildUri(final HttpContext context, final String uri) throws IOException {
static URI buildUri(HttpContext context, String uri) throws IOException {
try {
URIBuilder uriBuilder = new URIBuilder(uri);

Expand Down
Loading