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();