Skip to content

Commit

Permalink
Add support for Serverless OpenSearch signing
Browse files Browse the repository at this point in the history
Adds support for signing Serverless OpenSearch requests as described:
https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-clients.html#serverless-signing.

Requirements:
* You must specify the service name as aoss.
* You can't include Content-Length as a signed header, otherwise you'll
get an invalid signature error.
* Uses Aws4Signer while serverless uses  Aws4UnsignedPayloadSigner.

Removes rule to have `final` for all parameters as it create verbose
code while not enforcing strong enough protection. All parameters are
consider immutable as a good practice anyway.
  • Loading branch information
acm19 committed Jan 6, 2023
1 parent b61a1b2 commit 69829b4
Show file tree
Hide file tree
Showing 15 changed files with 425 additions and 90 deletions.
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 Serverless OpeanSearch - [@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 no 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))
|| 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 no 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

0 comments on commit 69829b4

Please sign in to comment.