-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Louis Chu <[email protected]>
- Loading branch information
Showing
7 changed files
with
318 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
...c/main/scala/org/opensearch/flint/core/auth/AWSRequestSigV4ASigningApacheInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.flint.core.auth; | ||
|
||
import com.amazonaws.auth.AWSSessionCredentials; | ||
import com.amazonaws.auth.AWSCredentialsProvider; | ||
import com.amazonaws.services.glue.model.InvalidStateException; | ||
import org.apache.http.Header; | ||
import org.apache.http.HttpHost; | ||
import org.apache.http.HttpRequest; | ||
import org.apache.http.HttpRequestInterceptor; | ||
import org.apache.http.client.utils.URIBuilder; | ||
import org.apache.http.message.BasicHeader; | ||
import org.apache.http.protocol.HttpContext; | ||
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; | ||
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; | ||
import software.amazon.awssdk.core.interceptor.ExecutionAttributes; | ||
import software.amazon.awssdk.core.signer.Signer; | ||
import software.amazon.awssdk.http.SdkHttpFullRequest; | ||
import software.amazon.awssdk.http.SdkHttpMethod; | ||
import software.amazon.awssdk.regions.Region; | ||
|
||
import java.io.IOException; | ||
import java.net.URISyntaxException; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.TreeMap; | ||
|
||
import static org.apache.http.protocol.HttpCoreContext.HTTP_TARGET_HOST; | ||
import static org.opensearch.flint.core.auth.AWSRequestSigningApacheInterceptor.nvpToMapParams; | ||
import static org.opensearch.flint.core.auth.AWSRequestSigningApacheInterceptor.skipHeader; | ||
|
||
/** | ||
* Interceptor for signing AWS requests according to Signature Version 4A. | ||
* This interceptor processes HTTP requests, signs them with AWS credentials, | ||
* and updates the request headers to include the signature. | ||
*/ | ||
public class AWSRequestSigV4ASigningApacheInterceptor implements HttpRequestInterceptor { | ||
private static final String HTTPS_PROTOCOL = "https"; | ||
private static final int HTTPS_PORT = 443; | ||
|
||
private final String service; | ||
private final String region; | ||
private final Signer signer; | ||
private final AWSCredentialsProvider awsCredentialsProvider; | ||
|
||
/** | ||
* Constructs an interceptor for AWS request signing with metadata access. | ||
* | ||
* @param service The AWS service name. | ||
* @param region The AWS region for signing. | ||
* @param signer The signer implementation. | ||
* @param awsCredentialsProvider The credentials provider for metadata access. | ||
*/ | ||
public AWSRequestSigV4ASigningApacheInterceptor(String service, String region, Signer signer, AWSCredentialsProvider awsCredentialsProvider) { | ||
this.service = service; | ||
this.region = region; | ||
this.signer = signer; | ||
this.awsCredentialsProvider = awsCredentialsProvider; | ||
} | ||
|
||
/** | ||
* Processes the HTTP request, signs it, and updates its headers. | ||
* | ||
* @param request The HTTP request to process and sign. | ||
* @param context The HTTP context. | ||
*/ | ||
@Override | ||
public void process(HttpRequest request, HttpContext context) throws IOException { | ||
SdkHttpFullRequest requestToSign = buildSdkHttpRequest(request, context); | ||
SdkHttpFullRequest signedRequest = signRequest(requestToSign); | ||
updateRequestHeaders(request, signedRequest.headers()); | ||
} | ||
|
||
private SdkHttpFullRequest buildSdkHttpRequest(HttpRequest request, HttpContext context) throws IOException { | ||
URIBuilder uriBuilder = parseUri(request); | ||
SdkHttpFullRequest.Builder builder = SdkHttpFullRequest.builder() | ||
.method(SdkHttpMethod.fromValue(request.getRequestLine().getMethod())) | ||
.port(HTTPS_PORT) | ||
.protocol(HTTPS_PROTOCOL) | ||
.headers(headerArrayToMap(request.getAllHeaders())) | ||
.rawQueryParameters(nvpToMapParams(uriBuilder.getQueryParams())); | ||
|
||
HttpHost host = (HttpHost) context.getAttribute(HTTP_TARGET_HOST); | ||
if (host == null) { | ||
throw new InvalidStateException("host must not be null"); | ||
} | ||
builder.host(host.toURI()); | ||
try { | ||
builder.encodedPath(uriBuilder.build().getRawPath()); | ||
} catch (URISyntaxException e) { | ||
throw new IOException("Invalid URI" , e); | ||
} | ||
return builder.build(); | ||
} | ||
|
||
private URIBuilder parseUri(HttpRequest request) throws IOException { | ||
try { | ||
return new URIBuilder(request.getRequestLine().getUri()); | ||
} catch (URISyntaxException e) { | ||
throw new IOException("Invalid URI", e); | ||
} | ||
} | ||
|
||
private SdkHttpFullRequest signRequest(SdkHttpFullRequest request) { | ||
AWSSessionCredentials sessionCredentials = (AWSSessionCredentials) awsCredentialsProvider.getCredentials(); | ||
AwsSessionCredentials awsCredentials = AwsSessionCredentials.create( | ||
sessionCredentials.getAWSAccessKeyId(), | ||
sessionCredentials.getAWSSecretKey(), | ||
sessionCredentials.getSessionToken() | ||
); | ||
|
||
ExecutionAttributes executionAttributes = new ExecutionAttributes() | ||
.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, awsCredentials) | ||
.putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, service) | ||
.putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, Region.of(region)); | ||
|
||
return signer.sign(request, executionAttributes); | ||
} | ||
|
||
private void updateRequestHeaders(HttpRequest request, Map<String, List<String>> signedHeaders) { | ||
Header[] headers = convertHeaderMapToArray(signedHeaders); | ||
request.setHeaders(headers); | ||
} | ||
|
||
/** | ||
* Converts an array of Headers into a map, accumulating multiple values for the same header name into a single list. | ||
* | ||
* @param headers The array of Headers to convert. | ||
* @return A map of header names to their corresponding list of values. | ||
*/ | ||
private static Map<String, List<String>> headerArrayToMap(final Header[] headers) { | ||
Map<String, List<String>> headersMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | ||
for (Header header : headers) { | ||
if (!skipHeader(header)) { | ||
headersMap.computeIfAbsent(header.getName(), k -> new ArrayList<>()).add(header.getValue()); | ||
} | ||
} | ||
return headersMap; | ||
} | ||
|
||
/** | ||
* Converts a map of header names to lists of values back into an array of Headers. | ||
* | ||
* @param mapHeaders The map of headers to convert. | ||
* @return An array of Headers. | ||
*/ | ||
private Header[] convertHeaderMapToArray(final Map<String, List<String>> mapHeaders) { | ||
return mapHeaders.entrySet().stream() | ||
.map(entry -> new BasicHeader(entry.getKey(), String.join(",", entry.getValue()))) | ||
.toArray(Header[]::new); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
...st/scala/org/opensearch/flint/core/auth/AWSRequestSigV4ASigningApacheInterceptorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package org.opensearch.flint.core.auth; | ||
|
||
import org.apache.http.HttpHost; | ||
import org.apache.http.HttpRequest; | ||
import org.apache.http.message.BasicHttpRequest; | ||
import org.apache.http.protocol.HttpContext; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.mockito.ArgumentCaptor; | ||
import org.mockito.Captor; | ||
import org.mockito.Mock; | ||
import org.mockito.MockitoAnnotations; | ||
|
||
import com.amazonaws.auth.AWSCredentialsProvider; | ||
import com.amazonaws.auth.AWSSessionCredentials; | ||
|
||
import software.amazon.awssdk.core.signer.Signer; | ||
import software.amazon.awssdk.http.SdkHttpFullRequest; | ||
import software.amazon.awssdk.http.SdkHttpMethod; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.TreeMap; | ||
|
||
import static org.apache.http.protocol.HttpCoreContext.HTTP_TARGET_HOST; | ||
import static org.junit.Assert.*; | ||
import static org.mockito.Mockito.*; | ||
|
||
public class AWSRequestSigV4ASigningApacheInterceptorTest { | ||
@Mock | ||
private Signer mockSigner; | ||
@Mock | ||
private AWSCredentialsProvider mockCredentialsProvider; | ||
@Mock | ||
private HttpContext mockContext; | ||
@Mock | ||
private AWSSessionCredentials mockSessionCredentials; | ||
@Mock | ||
private SdkHttpFullRequest mockSdkHttpFullRequest; | ||
@Captor | ||
private ArgumentCaptor<SdkHttpFullRequest> sdkHttpFullRequestCaptor; | ||
|
||
private AWSRequestSigV4ASigningApacheInterceptor interceptor; | ||
|
||
@Before | ||
public void setUp() { | ||
MockitoAnnotations.initMocks(this); | ||
when(mockCredentialsProvider.getCredentials()).thenReturn(mockSessionCredentials); | ||
when(mockSessionCredentials.getAWSAccessKeyId()).thenReturn("ACCESS_KEY_ID"); | ||
when(mockSessionCredentials.getAWSSecretKey()).thenReturn("SECRET_ACCESS_KEY"); | ||
when(mockSessionCredentials.getSessionToken()).thenReturn("SESSION_TOKEN"); | ||
interceptor = new AWSRequestSigV4ASigningApacheInterceptor("s3", "us-west-2", mockSigner, mockCredentialsProvider); | ||
when(mockContext.getAttribute(HTTP_TARGET_HOST)).thenReturn(new HttpHost("localhost", 443, "https")); | ||
when(mockSigner.sign(any(), any())).thenReturn(mockSdkHttpFullRequest); | ||
} | ||
|
||
@Test | ||
public void testSigningProcess() throws Exception { | ||
HttpRequest request = new BasicHttpRequest("GET", "/path/to/resource"); | ||
interceptor.process(request, mockContext); | ||
|
||
verify(mockSigner).sign(sdkHttpFullRequestCaptor.capture(), any()); | ||
SdkHttpFullRequest signedRequest = sdkHttpFullRequestCaptor.getValue(); | ||
|
||
assertEquals(SdkHttpMethod.GET, signedRequest.method()); | ||
assertEquals("/path/to/resource", signedRequest.encodedPath()); | ||
} | ||
|
||
@Test(expected = IOException.class) | ||
public void testInvalidUriHandling() throws Exception { | ||
HttpRequest request = new BasicHttpRequest("GET", ":///this/is/not/a/valid/uri"); | ||
interceptor.process(request, mockContext); | ||
} | ||
|
||
@Test | ||
public void testHeaderUpdateAfterSigning() throws Exception { | ||
// Setup mock signer to return a new SdkHttpFullRequest with an "Authorization" header | ||
when(mockSigner.sign(any(SdkHttpFullRequest.class), any())).thenAnswer(invocation -> { | ||
SdkHttpFullRequest originalRequest = invocation.getArgument(0); | ||
Map<String, List<String>> modifiedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | ||
modifiedHeaders.putAll(originalRequest.headers()); | ||
modifiedHeaders.put("Authorization", List.of("AWS4-HMAC-SHA256 Credential=...")); | ||
|
||
// Build a new SdkHttpFullRequest with the modified headers | ||
return SdkHttpFullRequest.builder() | ||
.method(originalRequest.method()) | ||
.uri(originalRequest.getUri()) | ||
.headers(modifiedHeaders) | ||
.build(); | ||
}); | ||
|
||
HttpRequest request = new BasicHttpRequest("GET", "/path/to/resource"); | ||
interceptor.process(request, mockContext); | ||
|
||
// Now verify that the HttpRequest has been updated with the new headers from the signed request | ||
assertTrue("The request does not contain the expected 'Authorization' header", | ||
request.containsHeader("Authorization")); | ||
assertEquals("AWS4-HMAC-SHA256 Credential=...", | ||
request.getFirstHeader("Authorization").getValue()); | ||
} | ||
} |
Oops, something went wrong.