Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit 058a0d7

Browse files
author
Corneil du Plessis
authored
Sanitize stream history data
Backport sanitizer (#4976)
1 parent 7d563cb commit 058a0d7

File tree

2 files changed

+139
-12
lines changed

2 files changed

+139
-12
lines changed

spring-cloud-dataflow-rest-resource/src/main/java/org/springframework/cloud/dataflow/rest/util/ArgumentSanitizer.java

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,27 @@
1717
package org.springframework.cloud.dataflow.rest.util;
1818

1919
import java.util.ArrayList;
20+
import java.util.Arrays;
2021
import java.util.HashMap;
2122
import java.util.LinkedHashMap;
2223
import java.util.List;
2324
import java.util.Map;
2425
import java.util.regex.Pattern;
2526

27+
import com.fasterxml.jackson.core.JsonProcessingException;
28+
import com.fasterxml.jackson.core.type.TypeReference;
29+
import com.fasterxml.jackson.databind.ObjectMapper;
30+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
2634
import org.springframework.batch.core.JobParameter;
2735
import org.springframework.batch.core.JobParameters;
2836
import org.springframework.cloud.dataflow.core.DefinitionUtils;
2937
import org.springframework.cloud.dataflow.core.TaskDefinition;
3038
import org.springframework.cloud.dataflow.core.dsl.TaskParser;
3139
import org.springframework.cloud.dataflow.core.dsl.graph.Graph;
40+
import org.springframework.http.HttpHeaders;
3241
import org.springframework.util.CollectionUtils;
3342
import org.springframework.util.StringUtils;
3443

@@ -40,13 +49,20 @@
4049
* @author Ilayaperumal Gopinathan
4150
*/
4251
public class ArgumentSanitizer {
52+
private final static Logger logger = LoggerFactory.getLogger(ArgumentSanitizer.class);
4353

44-
private static final String[] REGEX_PARTS = { "*", "$", "^", "+" };
54+
private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
4555

4656
private static final String REDACTION_STRING = "******";
4757

48-
private static final String[] KEYS_TO_SANITIZE = { "username", "password", "secret", "key", "token", ".*credentials.*",
49-
"vcap_services", "url" };
58+
private static final String[] KEYS_TO_SANITIZE = {"username", "password", "secret", "key", "token", ".*credentials.*",
59+
"vcap_services", "url"};
60+
61+
private final static TypeReference<Map<String, Object>> mapTypeReference = new TypeReference<Map<String, Object>>() {};
62+
63+
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
64+
65+
private final ObjectMapper jsonMapper = new ObjectMapper();
5066

5167
private Pattern[] keysToSanitize;
5268

@@ -80,6 +96,10 @@ private boolean isRegex(String value) {
8096
* @return the argument with a potentially sanitized value
8197
*/
8298
public String sanitize(String argument) {
99+
// Oracle handles an empty string as a null.
100+
if (argument == null) {
101+
return "";
102+
}
83103
int indexOfFirstEqual = argument.indexOf("=");
84104
if (indexOfFirstEqual == -1) {
85105
return argument;
@@ -95,7 +115,7 @@ public String sanitize(String argument) {
95115
/**
96116
* Replaces a potential secure value with "******".
97117
*
98-
* @param key to check for sensitive words.
118+
* @param key to check for sensitive words.
99119
* @param value the argument to cleanse.
100120
* @return the argument with a potentially sanitized value
101121
*/
@@ -118,13 +138,12 @@ public String sanitize(String key, String value) {
118138
* @return the sanitized job parameters
119139
*/
120140
public JobParameters sanitizeJobParameters(JobParameters jobParameters) {
121-
Map<String,JobParameter> newJobParameters = new HashMap<>();
122-
jobParameters.getParameters().forEach( (key, jobParameter) -> {
141+
Map<String, JobParameter> newJobParameters = new HashMap<>();
142+
jobParameters.getParameters().forEach((key, jobParameter) -> {
123143
String updatedKey = !jobParameter.isIdentifying() ? "-" + key : key;
124144
if (jobParameter.getType().equals(JobParameter.ParameterType.STRING)) {
125145
newJobParameters.put(updatedKey, new JobParameter(this.sanitize(key, jobParameter.toString())));
126-
}
127-
else {
146+
} else {
128147
newJobParameters.put(updatedKey, jobParameter);
129148
}
130149
});
@@ -138,7 +157,7 @@ public JobParameters sanitizeJobParameters(JobParameters jobParameters) {
138157
* @return Task definition text that has sensitive data redacted.
139158
*/
140159
public String sanitizeTaskDsl(TaskDefinition taskDefinition) {
141-
if(StringUtils.isEmpty(taskDefinition.getDslText())) {
160+
if (StringUtils.isEmpty(taskDefinition.getDslText())) {
142161
return taskDefinition.getDslText();
143162
}
144163
TaskParser taskParser = new TaskParser(taskDefinition.getTaskName(), taskDefinition.getDslText(), true, true);
@@ -147,7 +166,7 @@ public String sanitizeTaskDsl(TaskDefinition taskDefinition) {
147166
if (node.properties != null) {
148167
node.properties.keySet().stream().forEach(key -> {
149168
node.properties.put(key,
150-
DefinitionUtils.autoQuotes(sanitize(key, node.properties.get(key))));
169+
DefinitionUtils.autoQuotes(sanitize(key, node.properties.get(key))));
151170
});
152171
}
153172
});
@@ -157,13 +176,14 @@ public String sanitizeTaskDsl(TaskDefinition taskDefinition) {
157176
/**
158177
* For all sensitive properties (e.g. key names containing words like password, secret,
159178
* key, token) replace the value with '*****' string
179+
*
160180
* @param properties to be sanitized
161181
* @return sanitized properties
162182
*/
163183
public Map<String, String> sanitizeProperties(Map<String, String> properties) {
164184
if (!CollectionUtils.isEmpty(properties)) {
165185
final Map<String, String> sanitizedProperties = new LinkedHashMap<>(properties.size());
166-
for (Map.Entry<String, String > property : properties.entrySet()) {
186+
for (Map.Entry<String, String> property : properties.entrySet()) {
167187
sanitizedProperties.put(property.getKey(), this.sanitize(property.getKey(), property.getValue()));
168188
}
169189
return sanitizedProperties;
@@ -174,6 +194,7 @@ public Map<String, String> sanitizeProperties(Map<String, String> properties) {
174194
/**
175195
* For all sensitive arguments (e.g. key names containing words like password, secret,
176196
* key, token) replace the value with '*****' string
197+
*
177198
* @param arguments to be sanitized
178199
* @return sanitized arguments
179200
*/
@@ -187,4 +208,96 @@ public List<String> sanitizeArguments(List<String> arguments) {
187208
}
188209
return arguments;
189210
}
211+
212+
public HttpHeaders sanitizeHeaders(HttpHeaders headers) {
213+
HttpHeaders result = new HttpHeaders();
214+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
215+
List<String> values = entry.getValue();
216+
for (String value : values) {
217+
result.add(entry.getKey(), sanitize(entry.getKey(), value));
218+
}
219+
}
220+
return result;
221+
}
222+
223+
/**
224+
* Will replace sensitive string value in the Map with '*****'
225+
*
226+
* @param input to be sanitized
227+
* @return the sanitized map.
228+
*/
229+
public Map<String, Object> sanitizeMap(Map<String, Object> input) {
230+
Map<String, Object> result = new HashMap<>();
231+
for (Map.Entry<String, Object> entry : input.entrySet()) {
232+
if (entry.getValue() instanceof String) {
233+
result.put(entry.getKey(), sanitize(entry.getKey(), (String) entry.getValue()));
234+
} else if (entry.getValue() instanceof Map) {
235+
Map<String, Object> map = (Map<String, Object>) entry.getValue();
236+
result.put(entry.getKey(), sanitizeMap(map));
237+
} else {
238+
result.put(entry.getKey(), entry.getValue());
239+
}
240+
}
241+
return result;
242+
}
243+
244+
/**
245+
* Will replace the sensitive string fields with '*****'
246+
*
247+
* @param input to be sanitized
248+
* @return The sanitized JSON string
249+
* @throws JsonProcessingException
250+
*/
251+
public String sanitizeJsonString(String input) throws JsonProcessingException {
252+
if (input == null) {
253+
return null;
254+
}
255+
Map<String, Object> data = jsonMapper.readValue(input, mapTypeReference);
256+
return jsonMapper.writeValueAsString(sanitizeMap(data));
257+
}
258+
259+
/**
260+
* Will replace the sensitive string fields with '*****'
261+
*
262+
* @param input to be sanitized
263+
* @return The sanitized YAML string
264+
* @throws JsonProcessingException
265+
*/
266+
public String sanitizeYamlString(String input) throws JsonProcessingException {
267+
if (input == null) {
268+
return null;
269+
}
270+
Map<String, Object> data = yamlMapper.readValue(input, mapTypeReference);
271+
return yamlMapper.writeValueAsString(sanitizeMap(data));
272+
}
273+
274+
/**
275+
* Will determine the type of data and treat as JSON or YAML to sanitize sensitive values.
276+
*
277+
* @param input to be sanitized
278+
* @return the sanitized string
279+
* @throws JsonProcessingException
280+
*/
281+
public String sanitizeJsonOrYamlString(String input) {
282+
if (input == null) {
283+
return null;
284+
}
285+
try { // Try parsing as JSON
286+
return sanitizeJsonString(input);
287+
} catch (Throwable x) {
288+
logger.trace("Cannot parse as JSON:" + x);
289+
}
290+
try {
291+
return sanitizeYamlString(input);
292+
} catch (Throwable x) {
293+
logger.trace("Cannot parse as YAML:" + x);
294+
}
295+
if (input.contains("\n")) {
296+
return StringUtils.collectionToDelimitedString(sanitizeArguments(Arrays.asList(StringUtils.split(input, "\n"))), "\n");
297+
}
298+
if (input.contains("--")) {
299+
return StringUtils.collectionToDelimitedString(sanitizeArguments(Arrays.asList(StringUtils.split(input, "--"))), "--");
300+
}
301+
return sanitize(input);
302+
}
190303
}

spring-cloud-dataflow-server-core/src/main/java/org/springframework/cloud/dataflow/server/controller/StreamDeploymentController.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Arrays;
2020
import java.util.Collection;
2121
import java.util.Map;
22+
import java.util.stream.Collectors;
2223

2324
import org.slf4j.Logger;
2425
import org.slf4j.LoggerFactory;
@@ -29,6 +30,7 @@
2930
import org.springframework.cloud.dataflow.rest.UpdateStreamRequest;
3031
import org.springframework.cloud.dataflow.rest.resource.DeploymentStateResource;
3132
import org.springframework.cloud.dataflow.rest.resource.StreamDeploymentResource;
33+
import org.springframework.cloud.dataflow.rest.util.ArgumentSanitizer;
3234
import org.springframework.cloud.dataflow.server.controller.support.ControllerUtils;
3335
import org.springframework.cloud.dataflow.server.repository.NoSuchStreamDefinitionException;
3436
import org.springframework.cloud.dataflow.server.repository.StreamDefinitionRepository;
@@ -80,6 +82,8 @@ public class StreamDeploymentController {
8082
*/
8183
private final StreamDefinitionRepository repository;
8284

85+
private final ArgumentSanitizer sanitizer = new ArgumentSanitizer();
86+
8387
/**
8488
* Construct a new UpdatableStreamDeploymentController, given a
8589
* {@link StreamDeploymentController} and {@link StreamService}
@@ -142,7 +146,17 @@ public ResponseEntity<String> manifest(@PathVariable("name") String name,
142146
@RequestMapping(path = "/history/{name}", method = RequestMethod.GET)
143147
@ResponseStatus(HttpStatus.OK)
144148
public Collection<Release> history(@PathVariable("name") String releaseName) {
145-
return this.streamService.history(releaseName);
149+
return this.streamService.history(releaseName)
150+
.stream()
151+
.map(this::sanitizeRelease)
152+
.collect(Collectors.toList());
153+
}
154+
155+
private Release sanitizeRelease(Release release) {
156+
if (release.getConfigValues() != null && StringUtils.hasText(release.getConfigValues().getRaw())) {
157+
release.getConfigValues().setRaw(sanitizer.sanitizeJsonOrYamlString(release.getConfigValues().getRaw()));
158+
}
159+
return release;
146160
}
147161

148162
@RequestMapping(path = "/platform/list", method = RequestMethod.GET)

0 commit comments

Comments
 (0)