KNOX-2147 - Mask username/password in case we display call history and keep them safely (by setting proper file permissions) in JSON file (#217)
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java
index a2e5d7f..0543044 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/JDBCKnoxShellTableBuilder.java
@@ -26,6 +26,8 @@
import java.sql.Statement;
import java.util.Locale;
+import org.apache.commons.lang3.StringUtils;
+
public class JDBCKnoxShellTableBuilder extends KnoxShellTableBuilder {
private String connectionUrl;
@@ -107,15 +109,12 @@
return this.table;
}
- public Connection createConnection() throws SQLException {
- Connection con = null;
- if (username != null && pass != null) {
- con = DriverManager.getConnection(connectionUrl, username, pass);
+ private Connection createConnection() throws SQLException {
+ if (StringUtils.isNotBlank(username) && pass != null) {
+ return DriverManager.getConnection(connectionUrl, username, pass);
+ } else {
+ return DriverManager.getConnection(connectionUrl);
}
- else {
- con = DriverManager.getConnection(connectionUrl);
- }
- return con;
}
// added this as a private method so that KnoxShellTableHistoryAspect will not
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java
index 560ac20..591f8aa 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTable.java
@@ -19,12 +19,15 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.SortOrder;
import com.fasterxml.jackson.annotation.JsonFilter;
+
import org.apache.commons.math3.stat.StatUtils;
/**
@@ -307,7 +310,12 @@
}
public List<KnoxShellTableCall> getCallHistoryList() {
- return KnoxShellTableCallHistory.getInstance().getCallHistory(id);
+ final List<KnoxShellTableCall> sanitizedCallHistoryList = new LinkedList<>();
+ KnoxShellTableCallHistory.getInstance().getCallHistory(id).forEach(call -> {
+ final Map<Object, Class<?>> params = call.hasSensitiveData() ? Collections.singletonMap("***", String.class) : call.getParams();
+ sanitizedCallHistoryList.add(new KnoxShellTableCall(call.getInvokerClass(), call.getMethod(), call.isBuilderMethod(), params));
+ });
+ return sanitizedCallHistoryList;
}
public String getCallHistory() {
@@ -410,11 +418,19 @@
}
public String toJSON() {
- return toJSON(true);
+ return toJSON((String) null);
}
public String toJSON(boolean data) {
- return KnoxShellTableJSONSerializer.serializeKnoxShellTable(this, data);
+ return toJSON(data, null);
+ }
+
+ public String toJSON(String path) {
+ return toJSON(true, path);
+ }
+
+ public String toJSON(boolean data, String path) {
+ return KnoxShellTableJSONSerializer.serializeKnoxShellTable(this, data, path);
}
public String toCSV() {
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java
index 6ef5c2d..61bf6cd 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableCall.java
@@ -62,6 +62,11 @@
}
@JsonIgnore
+ boolean hasSensitiveData() {
+ return "username".equals(getMethod()) || "pwd".equals(getMethod());
+ }
+
+ @JsonIgnore
Class<?>[] getParameterTypes() {
final List<Class<?>> parameterTypes = new ArrayList<>(params.size());
if (KNOX_SHELL_TABLE_FILTER_TYPE.equals(invokerClass) && builderMethod) {
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java
index 7ea1ce3..24850e2 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/table/KnoxShellTableJSONSerializer.java
@@ -17,10 +17,17 @@
*/
package org.apache.knox.gateway.shell.table;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.shell.KnoxShellException;
import org.apache.knox.gateway.util.JsonUtils;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
@@ -51,15 +58,56 @@
* if this is <code>true</code> the underlying JSON serializer will
* output the table's content; otherwise the table's
* <code>callHistory</code> will be serilized
+ * @param filePath
+ * if set, the JSON result will be written into the given file
+ * (creating if not exists; overwritten if exists)
* @return the serialized table in JSON format
*/
- static String serializeKnoxShellTable(KnoxShellTable table, boolean data) {
- SimpleFilterProvider filterProvider = new SimpleFilterProvider();
- if (data) {
- filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("headers", "rows", "title", "id"));
+ static String serializeKnoxShellTable(KnoxShellTable table, boolean data, String filePath) {
+ if (StringUtils.isNotBlank(filePath)) {
+ return saveTableInFile(table, data, filePath);
} else {
- filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("callHistoryList"));
+ final SimpleFilterProvider filterProvider = new SimpleFilterProvider();
+ if (data) {
+ filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("headers", "rows", "title", "id"));
+ } else {
+ filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("callHistoryList"));
+ }
+ return JsonUtils.renderAsJsonString(table, filterProvider, JSON_DATE_FORMAT.get());
}
- return JsonUtils.renderAsJsonString(table, filterProvider, JSON_DATE_FORMAT.get());
+ }
+
+ private static String saveTableInFile(KnoxShellTable table, boolean data, String filePath) {
+ try {
+ final String jsonResult;
+ if (data) {
+ final SimpleFilterProvider filterProvider = new SimpleFilterProvider();
+ filterProvider.addFilter("knoxShellTableFilter", SimpleBeanPropertyFilter.filterOutAllExcept("headers", "rows", "title", "id"));
+ jsonResult = JsonUtils.renderAsJsonString(table, filterProvider, JSON_DATE_FORMAT.get());
+ } else {
+ jsonResult = JsonUtils.renderAsJsonString(KnoxShellTableCallHistory.getInstance().getCallHistory(table.id), null, JSON_DATE_FORMAT.get());
+ }
+ final Path jsonFilePath = Paths.get(filePath);
+ if (!Files.exists(jsonFilePath.getParent())) {
+ Files.createDirectories(jsonFilePath.getParent());
+ }
+ Files.deleteIfExists(jsonFilePath);
+ Files.createFile(jsonFilePath);
+ setPermissions(jsonFilePath);
+ Files.write(jsonFilePath, jsonResult.getBytes(StandardCharsets.UTF_8));
+ return "Successfully saved into " + filePath;
+ } catch (IOException e) {
+ throw new KnoxShellException("Error while saving KnoxShellTable JSON into " + filePath, e);
+ }
+ }
+
+ private static void setPermissions(Path path) throws IOException {
+ // clear all flags for everybody
+ path.toFile().setReadable(false, false);
+ path.toFile().setWritable(false, false);
+ path.toFile().setExecutable(false, false);
+ // allow owners to read/write
+ path.toFile().setReadable(true, true);
+ path.toFile().setWritable(true, true);
}
}
diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
index 84d1723..470cf4f 100644
--- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
+++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
@@ -18,6 +18,7 @@
package org.apache.knox.gateway.shell.table;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.singletonMap;
import static org.apache.commons.io.FileUtils.readFileToString;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
@@ -33,8 +34,11 @@
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermissions;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
@@ -42,6 +46,8 @@
import java.sql.Statement;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.SortOrder;
@@ -202,6 +208,15 @@
KnoxShellTable table2 = KnoxShellTable.builder().json().fromJson(json);
assertEquals(table.toString(), table2.toString());
+
+ final Path jsonPath = Paths.get(testFolder.newFolder().getAbsolutePath(), "testJson.json");
+ table.toJSON(jsonPath.toString());
+
+ final PosixFileAttributes jsonPathAttributes = Files.readAttributes(jsonPath, PosixFileAttributes.class);
+ assertEquals("rw-------", PosixFilePermissions.toString(jsonPathAttributes.permissions()));
+
+ KnoxShellTable table3 = KnoxShellTable.builder().json().path(jsonPath.toString());
+ assertEquals(table.toString(), table3.toString());
}
@Test
@@ -434,6 +449,32 @@
return derbyDatabase;
}
+ @Test
+ public void shouldMaskUserNameAndPasswordParametersWhenConnectingToDBUsingJDBCBuilder() throws Exception {
+ final KnoxShellTable table = new KnoxShellTable();
+ // it's quite hard to integrate AspectJ weaving together with JUnit so we
+ // manually build up the history
+ saveCall(table.id, "org.apache.knox.gateway.shell.table.KnoxShellTableBuilder", "jdbc", false, Collections.emptyMap());
+ saveCall(table.id, "org.apache.knox.gateway.shell.table.JDBCKnoxShellTableBuilder", "driver", false, singletonMap("org.apache.derby.jdbc.EmbeddedDriver", String.class));
+ saveCall(table.id, "org.apache.knox.gateway.shell.table.JDBCKnoxShellTableBuilder", "username", false, singletonMap("myUserName", String.class));
+ saveCall(table.id, "org.apache.knox.gateway.shell.table.JDBCKnoxShellTableBuilder", "pwd", false, singletonMap("myP4ssW0rd", String.class));
+ saveCall(table.id, "org.apache.knox.gateway.shell.table.JDBCKnoxShellTableBuilder", "connectTo", false, singletonMap("myDBConnectionUrl", String.class));
+ saveCall(table.id, "org.apache.knox.gateway.shell.table.JDBCKnoxShellTableBuilder", "sql", true, singletonMap("SELECT 1 FROM DUAL", String.class));
+ final AtomicBoolean foundSensitiveData = new AtomicBoolean(false);
+ table.getCallHistoryList().forEach(call -> {
+ if (call.hasSensitiveData()) {
+ assertEquals(1, call.getParams().size());
+ assertTrue(call.getParams().containsKey("***"));
+ foundSensitiveData.set(true);
+ }
+ });
+ assertTrue(foundSensitiveData.get());
+ }
+
+ private void saveCall(long id, String invokerClass, String method, boolean builderMethod, Map<Object, Class<?>>params) {
+ KnoxShellTableCallHistory.getInstance().saveCall(id, new KnoxShellTableCall(invokerClass, method, builderMethod, params));
+ }
+
@Test (expected = IllegalArgumentException.class)
public void testConversion() throws IllegalArgumentException {
KnoxShellTable TABLE = new KnoxShellTable();