Skip to content

Commit ae47de5

Browse files
committed
Added interceptor classes for supporting the "google-api-client" OpenAPI Generator library template
1 parent 36fdc0f commit ae47de5

File tree

7 files changed

+361
-0
lines changed

7 files changed

+361
-0
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ Library options currently supported for the `java` generator:
200200
+ [okhttp-gson](#okhttp-gson)
201201
+ [retrofit](#retrofit)
202202
+ [retrofit2](#retrofit2)
203+
+ [google-api-client](#google-api-client)
203204

204205
See also:
205206
* [OpenAPI Generator (maven Plugin)](https://mvnrepository.com/artifact/org.openapitools/openapi-generator-maven-plugin)
@@ -271,4 +272,31 @@ okBuilder.addNetworkInterceptor(new OkHttpFieldLevelEncryptionInterceptor(config
271272
okBuilder.addNetworkInterceptor(new OkHttpOAuth1Interceptor(consumerKey, signingKey));
272273
ServiceApi serviceApi = client.createService(ServiceApi.class);
273274
// ...
275+
```
276+
277+
#### google-api-client <a name="google-api-client"></a>
278+
##### OpenAPI Generator Plugin Configuration
279+
```xml
280+
<configuration>
281+
<inputSpec>${project.basedir}/src/main/resources/openapi-spec.yaml</inputSpec>
282+
<generatorName>java</generatorName>
283+
<library>google-api-client</library>
284+
<!-- ... -->
285+
</configuration>
286+
```
287+
288+
##### Usage of `HttpExecuteFieldLevelEncryptionInterceptor` and `HttpExecuteInterceptorChain`
289+
```java
290+
HttpRequestInitializer initializer = new HttpRequestInitializer() {
291+
@Override
292+
public void initialize(HttpRequest request) {
293+
HttpExecuteOAuth1Interceptor authenticationInterceptor = new HttpExecuteOAuth1Interceptor(consumerKey, signingKey);
294+
HttpExecuteFieldLevelEncryptionInterceptor encryptionInterceptor = new HttpExecuteFieldLevelEncryptionInterceptor(config);
295+
request.setInterceptor(new HttpExecuteInterceptorChain(Arrays.asList(encryptionInterceptor, authenticationInterceptor)));
296+
request.setResponseInterceptor(encryptionInterceptor);
297+
}
298+
};
299+
ApiClient client = new ApiClient("https://sandbox.api.mastercard.com", null, initializer, null);
300+
ServiceApi serviceApi = client.serviceApi();
301+
// ...
274302
```

pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@
106106
<version>4.12</version>
107107
<scope>test</scope>
108108
</dependency>
109+
110+
<dependency>
111+
<groupId>org.mockito</groupId>
112+
<artifactId>mockito-core</artifactId>
113+
<version>2.24.5</version>
114+
<scope>test</scope>
115+
</dependency>
116+
117+
<dependency>
118+
<groupId>commons-io</groupId>
119+
<artifactId>commons-io</artifactId>
120+
<version>2.6</version>
121+
<scope>test</scope>
122+
</dependency>
109123
</dependencies>
110124

111125
<profiles>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.mastercard.developer.interceptors;
2+
3+
import com.google.api.client.http.*;
4+
import com.mastercard.developer.encryption.EncryptionException;
5+
import com.mastercard.developer.encryption.FieldLevelEncryption;
6+
import com.mastercard.developer.encryption.FieldLevelEncryptionConfig;
7+
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.IOException;
10+
import java.lang.reflect.Field;
11+
import java.nio.charset.StandardCharsets;
12+
13+
/**
14+
* A Google Client API interceptor for encrypting/decrypting parts of HTTP payloads.
15+
* See also:
16+
* - {@link com.google.api.client.http.HttpExecuteInterceptor}
17+
* - {@link com.google.api.client.http.HttpResponseInterceptor}
18+
*/
19+
public class HttpExecuteFieldLevelEncryptionInterceptor implements HttpExecuteInterceptor, HttpResponseInterceptor {
20+
21+
private final FieldLevelEncryptionConfig config;
22+
23+
public HttpExecuteFieldLevelEncryptionInterceptor(FieldLevelEncryptionConfig config) {
24+
this.config = config;
25+
}
26+
27+
@Override
28+
public void intercept(HttpRequest request) throws IOException {
29+
try {
30+
// Check request actually has a payload
31+
HttpContent content = request.getContent();
32+
if (null == content || content.getLength() == 0) {
33+
// Nothing to encrypt
34+
return;
35+
}
36+
37+
// Read request payload
38+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
39+
content.writeTo(outputStream);
40+
String requestPayload = outputStream.toString(StandardCharsets.UTF_8.name());
41+
42+
// Encrypt fields
43+
String encryptedPayload = FieldLevelEncryption.encryptPayload(requestPayload, config);
44+
HttpContent encryptedContent = new ByteArrayContent("application/json; charset=" + StandardCharsets.UTF_8.name(), encryptedPayload.getBytes());
45+
request.getHeaders().setContentLength(encryptedContent.getLength());
46+
request.setContent(encryptedContent);
47+
48+
} catch (EncryptionException e) {
49+
throw new IOException("Failed to encrypt request!", e);
50+
}
51+
}
52+
53+
@Override
54+
public void interceptResponse(HttpResponse response) throws IOException {
55+
try {
56+
// Read response payload
57+
String responsePayload = response.parseAsString();
58+
if (null == responsePayload || responsePayload.length() == 0) {
59+
// Nothing to encrypt
60+
return;
61+
}
62+
63+
// Decrypt fields
64+
String decryptedPayload = FieldLevelEncryption.decryptPayload(responsePayload, config);
65+
HttpContent decryptedContent = new ByteArrayContent("application/json; charset=" + StandardCharsets.UTF_8.name(), decryptedPayload.getBytes());
66+
response.getHeaders().setContentLength(decryptedContent.getLength());
67+
68+
// The HttpResponse public interface prevent from updating the response payload:
69+
// "Do not read from the content stream unless you intend to throw an exception"
70+
Field contentField = response.getClass().getDeclaredField("content");
71+
contentField.setAccessible(true);
72+
contentField.set(response, ((ByteArrayContent) decryptedContent).getInputStream());
73+
74+
} catch (EncryptionException e) {
75+
throw new IOException("Failed to decrypt response!", e);
76+
} catch (NoSuchFieldException | IllegalAccessException e) {
77+
throw new IOException("Failed to update response with decrypted payload!", e);
78+
}
79+
}
80+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.mastercard.developer.interceptors;
2+
3+
import com.google.api.client.http.HttpExecuteInterceptor;
4+
import com.google.api.client.http.HttpRequest;
5+
6+
import java.io.IOException;
7+
import java.util.List;
8+
9+
/**
10+
* Helper to chain multiple Google Client API request interceptors.
11+
*/
12+
public class HttpExecuteInterceptorChain implements HttpExecuteInterceptor {
13+
14+
private final List<HttpExecuteInterceptor> requestInterceptors;
15+
16+
public HttpExecuteInterceptorChain(List<HttpExecuteInterceptor> requestInterceptors) {
17+
this.requestInterceptors = requestInterceptors;
18+
}
19+
20+
@Override
21+
public void intercept(HttpRequest request) throws IOException {
22+
for (HttpExecuteInterceptor interceptor: requestInterceptors) {
23+
interceptor.intercept(request);
24+
}
25+
}
26+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package com.mastercard.developer.interceptor;
2+
3+
import com.google.api.client.http.*;
4+
import com.mastercard.developer.encryption.EncryptionException;
5+
import com.mastercard.developer.encryption.FieldLevelEncryptionConfig;
6+
import com.mastercard.developer.interceptors.HttpExecuteFieldLevelEncryptionInterceptor;
7+
import org.apache.commons.io.IOUtils;
8+
import org.junit.Assert;
9+
import org.junit.Rule;
10+
import org.junit.Test;
11+
import org.junit.rules.ExpectedException;
12+
import org.junit.runner.RunWith;
13+
import org.mockito.ArgumentCaptor;
14+
import org.mockito.junit.MockitoJUnitRunner;
15+
import sun.security.x509.X509CertImpl;
16+
17+
import java.io.ByteArrayOutputStream;
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.lang.reflect.Field;
21+
import java.nio.charset.StandardCharsets;
22+
23+
import static com.mastercard.developer.test.TestUtils.getFieldLevelEncryptionConfigBuilder;
24+
import static org.hamcrest.core.Is.isA;
25+
import static org.mockito.Mockito.*;
26+
27+
@RunWith(MockitoJUnitRunner.class)
28+
public class HttpExecuteFieldLevelEncryptionInterceptorTest {
29+
30+
private static final String JSON_TYPE = "application/json; charset=utf-8";
31+
32+
@Rule
33+
public ExpectedException expectedException = ExpectedException.none();
34+
35+
@Test
36+
public void testIntercept_ShouldEncryptRequestPayload() throws Exception {
37+
38+
// GIVEN
39+
FieldLevelEncryptionConfig config = getFieldLevelEncryptionConfigBuilder()
40+
.withEncryptionPath("$.foo", "$.encryptedFoo")
41+
.build();
42+
HttpRequest request = mock(HttpRequest.class);
43+
HttpHeaders httpHeaders = new HttpHeaders();
44+
when(request.getContent()).thenReturn(new ByteArrayContent(JSON_TYPE, "{\"foo\":\"bar\"}".getBytes()));
45+
when(request.getHeaders()).thenReturn(httpHeaders);
46+
47+
// WHEN
48+
HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config);
49+
instanceUnderTest.intercept(request);
50+
51+
// THEN
52+
ArgumentCaptor<HttpContent> contentCaptor = ArgumentCaptor.forClass(HttpContent.class);
53+
verify(request).setContent(contentCaptor.capture());
54+
ByteArrayOutputStream encryptedPayloadStream = new ByteArrayOutputStream();
55+
contentCaptor.getValue().writeTo(encryptedPayloadStream);
56+
String encryptedPayload = encryptedPayloadStream.toString(StandardCharsets.UTF_8.name());
57+
Assert.assertFalse(encryptedPayload.contains("foo"));
58+
Assert.assertTrue(encryptedPayload.contains("encryptedFoo"));
59+
Assert.assertEquals(868, httpHeaders.getContentLength().intValue());
60+
}
61+
62+
@Test
63+
public void testIntercept_ShouldDoNothing_WhenNoPayload() throws Exception {
64+
65+
// GIVEN
66+
FieldLevelEncryptionConfig config = getFieldLevelEncryptionConfigBuilder()
67+
.withEncryptionPath("$.foo", "$.encryptedFoo")
68+
.build();
69+
HttpRequest request = mock(HttpRequest.class);
70+
when(request.getContent()).thenReturn(null);
71+
72+
// WHEN
73+
HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config);
74+
instanceUnderTest.intercept(request);
75+
76+
// THEN
77+
verify(request).getContent();
78+
verifyNoMoreInteractions(request);
79+
}
80+
81+
@Test
82+
public void testIntercept_ShouldThrowIOException_WhenEncryptionFails() throws Exception {
83+
84+
// GIVEN
85+
FieldLevelEncryptionConfig config = getFieldLevelEncryptionConfigBuilder()
86+
.withEncryptionPath("$.foo", "$.encryptedFoo")
87+
.withEncryptionCertificate(new X509CertImpl()) // Certificate without key
88+
.build();
89+
HttpRequest request = mock(HttpRequest.class);
90+
when(request.getContent()).thenReturn(new ByteArrayContent(JSON_TYPE, "{\"foo\":\"bar\"}".getBytes()));
91+
92+
// THEN
93+
expectedException.expect(IOException.class);
94+
expectedException.expectMessage("Failed to encrypt request!");
95+
expectedException.expectCause(isA(EncryptionException.class));
96+
97+
// WHEN
98+
HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config);
99+
instanceUnderTest.intercept(request);
100+
}
101+
102+
@Test
103+
public void testInterceptResponse_ShouldDecryptResponsePayload() throws Exception {
104+
105+
// GIVEN
106+
String encryptedPayload = "{" +
107+
" \"encryptedData\": {" +
108+
" \"iv\": \"a2c494ca28dec4f3d6ce7d68b1044cfe\"," +
109+
" \"encryptedKey\": \"038c65f154a2b07f6c788aaddc13ecead05fdeb11eca0bf576cab7185df66349d2cba4ba51a5304d45995e915bb1de462f0f66acd05026b21340b567d141653a2175ccfe2030b3b49261c6750381421cf0e29bd67840bcdc8092a44691c6c74dcdf620d5a744832fce3b45b8e3f8ad1af6c919195eb7f878c7435143e328e8b858dd232dbfacf7bb2f73981a80a09dc7c6dcd49ad95df527d415438958700b48994d7f6207f03d974a5cf50181205ac0a301a91e94b35ad162c8cf39475d2505d8ae7b1d4ed6f170091ab523f037a75eddb5ca46db9328c10395b69f8b798c280fa0e76f8385a64fe37b67e8578f3f9572dfb87d71e80a97323753030966901b\"," +
110+
" \"encryptedValue\": \"0672589113046bf692265b6ea6088184\"," +
111+
" \"oaepHashingAlgorithm\": \"SHA256\"" +
112+
" }" +
113+
"}";
114+
FieldLevelEncryptionConfig config = getFieldLevelEncryptionConfigBuilder()
115+
.withDecryptionPath("$.encryptedData", "$.data")
116+
.build();
117+
HttpResponse response = mock(HttpResponse.class);
118+
HttpHeaders httpHeaders = new HttpHeaders();
119+
when(response.parseAsString()).thenReturn(encryptedPayload);
120+
when(response.getHeaders()).thenReturn(httpHeaders);
121+
122+
// WHEN
123+
HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config);
124+
instanceUnderTest.interceptResponse(response);
125+
126+
// THEN
127+
String expectedPayload = "{\"data\":\"string\"}";
128+
Field contentField = response.getClass().getDeclaredField("content");
129+
contentField.setAccessible(true);
130+
InputStream payloadInputStream = (InputStream) contentField.get(response);
131+
Assert.assertEquals(expectedPayload, IOUtils.toString(payloadInputStream, StandardCharsets.UTF_8));
132+
Assert.assertEquals(expectedPayload.length(), httpHeaders.getContentLength().intValue());
133+
}
134+
135+
@Test
136+
public void testInterceptResponse_ShouldDoNothing_WhenNoPayload() throws Exception {
137+
138+
// GIVEN
139+
FieldLevelEncryptionConfig config = getFieldLevelEncryptionConfigBuilder().build();
140+
HttpResponse response = mock(HttpResponse.class);
141+
when(response.parseAsString()).thenReturn(null);
142+
143+
// WHEN
144+
HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config);
145+
instanceUnderTest.interceptResponse(response);
146+
147+
// THEN
148+
verify(response).parseAsString();
149+
verifyNoMoreInteractions(response);
150+
}
151+
152+
@Test
153+
public void testInterceptResponse_ShouldThrowIOException_WhenDecryptionFails() throws Exception {
154+
155+
// GIVEN
156+
String encryptedPayload = "{" +
157+
" \"encryptedData\": {" +
158+
" \"iv\": \"a2c494ca28dec4f3d6ce7d68b1044cfe\"," +
159+
" \"encryptedKey\": \"Not a valid key\"," +
160+
" \"encryptedValue\": \"0672589113046bf692265b6ea6088184\"" +
161+
" }" +
162+
"}";
163+
FieldLevelEncryptionConfig config = getFieldLevelEncryptionConfigBuilder()
164+
.withDecryptionPath("$.encryptedData", "$.data")
165+
.build();
166+
HttpResponse response = mock(HttpResponse.class);
167+
when(response.parseAsString()).thenReturn(encryptedPayload);
168+
169+
// THEN
170+
expectedException.expect(IOException.class);
171+
expectedException.expectMessage("Failed to decrypt response!");
172+
expectedException.expectCause(isA(EncryptionException.class));
173+
174+
// WHEN
175+
HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config);
176+
instanceUnderTest.interceptResponse(response);
177+
}
178+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.mastercard.developer.interceptor;
2+
3+
import com.google.api.client.http.HttpExecuteInterceptor;
4+
import com.google.api.client.http.HttpRequest;
5+
import com.mastercard.developer.interceptors.HttpExecuteInterceptorChain;
6+
import org.junit.Test;
7+
8+
import java.io.IOException;
9+
import java.util.Arrays;
10+
import java.util.List;
11+
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.verify;
14+
15+
public class HttpExecuteInterceptorChainTest {
16+
17+
@Test
18+
public void testIntercept() throws IOException {
19+
20+
// GIVEN
21+
HttpExecuteInterceptor interceptor1 = mock(HttpExecuteInterceptor.class);
22+
HttpExecuteInterceptor interceptor2 = mock(HttpExecuteInterceptor.class);
23+
List<HttpExecuteInterceptor> interceptors = Arrays.asList(interceptor1, interceptor2);
24+
HttpRequest request = mock(HttpRequest.class);
25+
26+
// WHEN
27+
HttpExecuteInterceptorChain instanceUnderTest = new HttpExecuteInterceptorChain(interceptors);
28+
instanceUnderTest.intercept(request);
29+
30+
// THEN
31+
verify(interceptor1).intercept(request);
32+
verify(interceptor2).intercept(request);
33+
}
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock-maker-inline

0 commit comments

Comments
 (0)