Skip to content

Commit 9e09e7d

Browse files
author
Joe Wolf
committed
Lock down CloudFormationResponse; default SdkHttpClient
- Include powertools-cloudformation in .github config - Address mutatable ObjectMapper spotbugs finding - JavaDoc cleanup
1 parent c13ea3a commit 9e09e7d

File tree

9 files changed

+122
-26
lines changed

9 files changed

+122
-26
lines changed

.github/workflows/build.yml

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- master
77
paths:
8+
- 'powertools-cloudformation/**'
89
- 'powertools-core/**'
910
- 'powertools-logging/**'
1011
- 'powertools-sqs/**'
@@ -18,6 +19,7 @@ on:
1819
branches:
1920
- master
2021
paths:
22+
- 'powertools-cloudformation/**'
2123
- 'powertools-core/**'
2224
- 'powertools-logging/**'
2325
- 'powertools-sqs/**'

.github/workflows/spotbugs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- master
77
paths:
8+
- 'powertools-cloudformation/**'
89
- 'powertools-core/**'
910
- 'powertools-logging/**'
1011
- 'powertools-sqs/**'

pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
<artifactId>http-client-spi</artifactId>
112112
<version>${aws.sdk.version}</version>
113113
</dependency>
114+
<dependency>
115+
<groupId>software.amazon.awssdk</groupId>
116+
<artifactId>url-connection-client</artifactId>
117+
<version>${aws.sdk.version}</version>
118+
</dependency>
114119
<dependency>
115120
<groupId>io.burt</groupId>
116121
<artifactId>jmespath-jackson</artifactId>

powertools-cloudformation/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
<groupId>software.amazon.awssdk</groupId>
4747
<artifactId>http-client-spi</artifactId>
4848
</dependency>
49+
<dependency>
50+
<groupId>software.amazon.awssdk</groupId>
51+
<artifactId>url-connection-client</artifactId>
52+
</dependency>
4953
<dependency>
5054
<groupId>com.amazonaws</groupId>
5155
<artifactId>aws-lambda-java-core</artifactId>

powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java

+14-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.slf4j.Logger;
77
import org.slf4j.LoggerFactory;
88
import software.amazon.awssdk.http.SdkHttpClient;
9+
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
910

1011
import java.io.IOException;
1112
import java.util.Objects;
@@ -22,12 +23,19 @@ public abstract class AbstractCustomResourceHandler
2223

2324
private final SdkHttpClient client;
2425

26+
/**
27+
* Creates a new Handler that uses the default HTTP client for communicating with custom CloudFormation resources.
28+
*/
29+
protected AbstractCustomResourceHandler() {
30+
this.client = UrlConnectionHttpClient.create();
31+
}
32+
2533
/**
2634
* Creates a new Handler that uses the provided HTTP client for communicating with custom CloudFormation resources.
2735
*
2836
* @param client cannot be null
2937
*/
30-
public AbstractCustomResourceHandler(SdkHttpClient client) {
38+
protected AbstractCustomResourceHandler(SdkHttpClient client) {
3139
this.client = Objects.requireNonNull(client, "SdkHttpClient cannot be null.");
3240
}
3341

@@ -52,16 +60,15 @@ public final Response handleRequest(CloudFormationCustomResourceEvent event, Con
5260
LOG.debug("Preparing to send response {} to {}.", response, responseUrl);
5361
client.send(event, context, response);
5462
} catch (IOException ioe) {
55-
LOG.error("Unable to send {} success to {}.", responseUrl, ioe);
63+
LOG.error("Unable to send response {} to {}.", response, responseUrl, ioe);
5664
onSendFailure(event, context, response, ioe);
5765
} catch (CustomResourceResponseException rse) {
58-
LOG.error("Unable to create/serialize Response. Sending empty failure to {}", responseUrl, rse);
59-
// send a failure with a null response on account of response serialization issues
66+
LOG.error("Unable to generate response. Sending empty failure to {}", responseUrl, rse);
6067
try {
6168
client.send(event, context, Response.failed());
6269
} catch (Exception e) {
63-
// unable to serialize response AND send an empty response
64-
LOG.error("Unable to send empty failure to {}.", responseUrl, e);
70+
// unable to generate response AND send the failure
71+
LOG.error("Unable to send failure response to {}.", responseUrl, e);
6572
onSendFailure(event, context, null, e);
6673
}
6774
}
@@ -92,7 +99,7 @@ private Response getResponse(CloudFormationCustomResourceEvent event, Context co
9299
*
93100
* @return a client for sending the response
94101
*/
95-
protected CloudFormationResponse buildResponseClient() {
102+
CloudFormationResponse buildResponseClient() {
96103
return new CloudFormationResponse(client);
97104
}
98105

powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* <p>
3131
* This class is thread-safe provided the SdkHttpClient instance used is also thread-safe.
3232
*/
33-
public class CloudFormationResponse {
33+
class CloudFormationResponse {
3434

3535
/**
3636
* Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload
@@ -120,10 +120,19 @@ ObjectNode toObjectNode(JsonNode dataNode) {
120120
*
121121
* @param client HTTP client to use for sending requests; cannot be null
122122
*/
123-
public CloudFormationResponse(SdkHttpClient client) {
123+
CloudFormationResponse(SdkHttpClient client) {
124124
this.client = Objects.requireNonNull(client, "SdkHttpClient cannot be null");
125125
}
126126

127+
/**
128+
* The underlying SdkHttpClient used by this class.
129+
*
130+
* @return a non-null client
131+
*/
132+
SdkHttpClient getClient() {
133+
return client;
134+
}
135+
127136
/**
128137
* Forwards a response containing a custom payload to the target resource specified by the event. The payload is
129138
* formed from the event and context data. Status is assumed to be SUCCESS.

powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java

+11-9
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
public class Response {
1515

1616
/**
17-
* Indicates whether this response should be sent to the custom resource as a success or failure.
17+
* Indicates whether a response is a success or failure.
1818
*/
1919
public enum Status {
2020
SUCCESS, FAILED
2121
}
2222

2323
/**
24-
* For building Response instances that wrap a single, arbitrary value.
24+
* For building Response instances.
2525
*/
2626
public static class Builder {
2727
private Object value;
@@ -45,13 +45,15 @@ public Builder value(Object value) {
4545
}
4646

4747
/**
48-
* Configures a custom ObjectMapper for serializing the value object.
48+
* Configures a custom ObjectMapper for serializing the value object. Creates a copy of the mapper provided;
49+
* future mutations of the ObjectMapper made using the provided reference will not affect Response
50+
* serialization.
4951
*
5052
* @param objectMapper if null, a default mapper will be used
5153
* @return a reference to this builder
5254
*/
5355
public Builder objectMapper(ObjectMapper objectMapper) {
54-
this.objectMapper = objectMapper;
56+
this.objectMapper = objectMapper == null ? null : objectMapper.copy();
5557
return this;
5658
}
5759

@@ -166,7 +168,7 @@ private Response(JsonNode jsonNode, Status status, String physicalResourceId, bo
166168
}
167169

168170
/**
169-
* Returns a JsonNode representation of the response.
171+
* Returns a JsonNode representation of the Response.
170172
*
171173
* @return a non-null JsonNode representation
172174
*/
@@ -175,7 +177,7 @@ JsonNode getJsonNode() {
175177
}
176178

177179
/**
178-
* The success/failed status of the response.
180+
* The success/failed status of the Response.
179181
*
180182
* @return a non-null Status
181183
*/
@@ -184,7 +186,7 @@ public Status getStatus() {
184186
}
185187

186188
/**
187-
* The physical resource ID of the custom resource. If null, the default resource ID will be used.
189+
* The physical resource ID. If null, the default physical resource ID will be provided to the custom resource.
188190
*
189191
* @return a potentially null physical resource ID
190192
*/
@@ -202,9 +204,9 @@ public boolean isNoEcho() {
202204
}
203205

204206
/**
205-
* The Response JSON.
207+
* Includes all Response attributes, including its value in JSON format
206208
*
207-
* @return a String in JSON format
209+
* @return a full description of the Response
208210
*/
209211
@Override
210212
public String toString() {

powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java

+41-8
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,14 @@
2424
public class AbstractCustomResourceHandlerTest {
2525

2626
/**
27-
* Uses a mocked CloudFormationResponse to avoid sending actual HTTP requests.
27+
* Bare-bones implementation that returns null for abstract methods.
2828
*/
29-
static class NoOpCustomResourceHandler extends AbstractCustomResourceHandler {
30-
31-
NoOpCustomResourceHandler() {
32-
super(mock(SdkHttpClient.class));
29+
static class NullCustomResourceHandler extends AbstractCustomResourceHandler {
30+
NullCustomResourceHandler() {
3331
}
3432

35-
@Override
36-
protected CloudFormationResponse buildResponseClient() {
37-
return mock(CloudFormationResponse.class);
33+
NullCustomResourceHandler(SdkHttpClient client) {
34+
super(client);
3835
}
3936

4037
@Override
@@ -53,6 +50,21 @@ protected Response delete(CloudFormationCustomResourceEvent event, Context conte
5350
}
5451
}
5552

53+
/**
54+
* Uses a mocked CloudFormationResponse to avoid sending actual HTTP requests.
55+
*/
56+
static class NoOpCustomResourceHandler extends NullCustomResourceHandler {
57+
58+
NoOpCustomResourceHandler() {
59+
super(mock(SdkHttpClient.class));
60+
}
61+
62+
@Override
63+
protected CloudFormationResponse buildResponseClient() {
64+
return mock(CloudFormationResponse.class);
65+
}
66+
}
67+
5668
/**
5769
* Creates a handler that will expect the Response to be sent with an expected status. Will throw an AssertionError
5870
* if the method is sent with an unexpected status.
@@ -109,6 +121,27 @@ static CloudFormationCustomResourceEvent eventOfType(String requestType) {
109121
return event;
110122
}
111123

124+
@Test
125+
void defaultAndCustomSdkHttpClients() {
126+
AbstractCustomResourceHandler defaultClientHandler = new NullCustomResourceHandler();
127+
128+
SdkHttpClient defaultClient = defaultClientHandler.buildResponseClient().getClient();
129+
assertThat(defaultClient).isNotNull();
130+
131+
String customClientName = "mockCustomClient";
132+
SdkHttpClient customClientArg = mock(SdkHttpClient.class);
133+
when(customClientArg.clientName()).thenReturn(customClientName);
134+
AbstractCustomResourceHandler customClientHandler = new NullCustomResourceHandler(customClientArg);
135+
136+
SdkHttpClient customClient = customClientHandler.buildResponseClient().getClient();
137+
assertThat(customClient).isNotNull();
138+
assertThat(customClient.clientName())
139+
.isEqualTo(customClientName);
140+
141+
assertThat(customClient.clientName())
142+
.isNotEqualTo(defaultClient.clientName());
143+
}
144+
112145
@ParameterizedTest
113146
@CsvSource(value = {"Create,1,0,0", "Update,0,1,0", "Delete,0,0,1"}, delimiter = ',')
114147
void eventsDelegateToCorrectHandlerMethod(String eventType, int createCount, int updateCount, int deleteCount) {

powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ void jsonObjectValueWithDefaultObjectMapper() {
109109
assertThat(response.toString()).contains("JSON = " + expected);
110110
}
111111

112+
@Test
113+
void jsonObjectValueWithNullObjectMapper() {
114+
DummyBean value = new DummyBean("test");
115+
116+
Response response = Response.builder()
117+
.objectMapper(null)
118+
.value(value)
119+
.build();
120+
121+
String expected = "{\"PropertyWithLongName\":\"test\"}";
122+
assertThat(response.getJsonNode().toString()).isEqualTo(expected);
123+
assertThat(response.toString()).contains("JSON = " + expected);
124+
}
125+
112126
@Test
113127
void jsonObjectValueWithCustomObjectMapper() {
114128
ObjectMapper customMapper = new ObjectMapper()
@@ -125,6 +139,25 @@ void jsonObjectValueWithCustomObjectMapper() {
125139
assertThat(response.toString()).contains("JSON = " + expected);
126140
}
127141

142+
@Test
143+
void jsonObjectValueWithPostConfiguredObjectMapper() {
144+
ObjectMapper customMapper = new ObjectMapper()
145+
.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE);
146+
147+
DummyBean value = new DummyBean(10);
148+
Response response = Response.builder()
149+
.objectMapper(customMapper)
150+
.value(value)
151+
.build();
152+
153+
// changing the mapper config should not affect serialization
154+
customMapper.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE);
155+
156+
String expected = "{\"property-with-long-name\":10}";
157+
assertThat(response.getJsonNode().toString()).isEqualTo(expected);
158+
assertThat(response.toString()).contains("JSON = " + expected);
159+
}
160+
128161
@Test
129162
void successFactoryMethod() {
130163
Response response = Response.success();

0 commit comments

Comments
 (0)