DRILL-8232: Add support for user credentials to VaultCredentialsProvider (#2558)

* Add support for user credentials to VaultCredentialsProvider.

* Change LOGGER var to lower case.

* Convert VaultCredentialsProvider to AppRole authentication.

BREAKING. VaultCredentialsProvider was previously configured with
a static Vault token in a BOOT option. Now it is configured with a
static AppRole and secret in BOOT options and those are used to obtain
Vault tokens.

* Fox logic and style.

* Satisfy RAT.
diff --git a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestUserTranslationInHttpPlugin.java b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestUserTranslationInHttpPlugin.java
index f4d2001..b909b80 100644
--- a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestUserTranslationInHttpPlugin.java
+++ b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestUserTranslationInHttpPlugin.java
@@ -172,7 +172,7 @@
     StoragePluginRegistry registry = cluster.storageRegistry();
     StoragePlugin plugin = registry.getPlugin("local");
     PlainCredentialsProvider credentialsProvider = (PlainCredentialsProvider) plugin.getConfig().getCredentialsProvider();
-    Map<String, String> credentials = credentialsProvider.getCredentials(TEST_USER_1);
+    Map<String, String> credentials = credentialsProvider.getUserCredentials(TEST_USER_1);
     assertNotNull(credentials);
     assertNull(credentials.get("username"));
     assertNull(credentials.get("password"));
diff --git a/docs/dev/PluginCredentialsProvider.md b/docs/dev/PluginCredentialsProvider.md
index 7702d1e..7855f10 100644
--- a/docs/dev/PluginCredentialsProvider.md
+++ b/docs/dev/PluginCredentialsProvider.md
@@ -93,14 +93,17 @@
 
 ## Using credentials managed by Vault
 
-`VaultCredentialsProvider` credentials provider implementation allows using Vault secrets as plugin credentials.
+`VaultCredentialsProvider` credentials provider implementation allows using Vault secrets as plugin credentials. Currently, this credentials provider authenticates itself to Vault using the [AppRole](https://www.vaultproject.io/docs/auth/approle) auth method which is intended for use by applications and services. In future, it may be able to use the Vault token of the Drill query user instead, in the event that the `VaultUserAuthenticator` is also in use.
 
 Before using this credential provider, the following Drill properties should be configured in `drill-override.conf`:
 ```
-"drill.exec.storage.vault.address" - address of the Vault server
-"drill.exec.storage.vault.token" - token used to access Vault
+"drill.exec.storage.vault.address" - host name or address of the Vault server.
+"drill.exec.storage.vault.app_role_id" - the role ID belonging to the AppRole Drill will use
+"drill.exec.storage.vault.secret_id" - the secret ID belonging to the AppRole Drill will use
 ```
 
+Note that you will generally need to create and assign a [Vault policy](https://www.hashicorp.com/resources/policies-vault) to grant the AppRole used by Drill read access to Vault secrets.
+
 Once it is set, we can configure storage plugin to use this way of obtaining credentials:
 ```json
 {
@@ -118,7 +121,7 @@
 }
 ```
 
-`secretPath` property specifies the Vault key value from which to read
+`secretPath` property specifies the Vault key value from which to read. If the plugin's `authMode` is set to `user_translation` then the `secretPath` may include a variable named `$user` which will be replaced with the Drill query username at query execution time.
 `propertyNames` map contains keys that specify which credential will be obtained from the Vault secret with the secret name of the `propertyNames` value.
 
 For example, user may store the following secrets in the Vault:
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
index 9ae3482..64972c4 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
@@ -64,22 +64,22 @@
   }
 
   @JsonIgnore
-  public String getUserName(String activeUser) {
+  public String getUserName(String queryUser) {
     CredentialsProvider credentialsProvider = config.getCredentialsProvider();
     Optional<UsernamePasswordCredentials> credentials = new UsernamePasswordCredentials.Builder()
       .setCredentialsProvider(credentialsProvider)
-      .setQueryUser(activeUser)
+      .setQueryUser(queryUser)
       .build();
 
     return credentials.map(UsernamePasswordCredentials::getUsername).orElse(null);
   }
 
   @JsonIgnore
-  public String getPassword(String activeUser) {
+  public String getPassword(String queryUser) {
     CredentialsProvider credentialsProvider = config.getCredentialsProvider();
     Optional<UsernamePasswordCredentials> credentials = new UsernamePasswordCredentials.Builder()
       .setCredentialsProvider(credentialsProvider)
-      .setQueryUser(activeUser)
+      .setQueryUser(queryUser)
       .build();
 
     return credentials.map(UsernamePasswordCredentials::getPassword).orElse(null);
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/EnvCredentialsProvider.java b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/EnvCredentialsProvider.java
index 2325018..03e866c 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/EnvCredentialsProvider.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/EnvCredentialsProvider.java
@@ -42,8 +42,12 @@
   @Override
   public Map<String, String> getCredentials() {
     Map<String, String> credentials = new HashMap<>();
-    envVariables.forEach((key, value) -> credentials.put(key, System.getenv(value)));
-
+    envVariables.forEach((key, value) -> {
+      String cred = System.getenv(value);
+      if (cred != null) {
+        credentials.put(key, cred);
+      }
+    });
     return credentials;
   }
 
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
index 3799305..9fa663d 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordCredentials.java
@@ -19,15 +19,12 @@
 
 import org.apache.drill.common.PlanStringBuilder;
 import org.apache.drill.common.logical.security.CredentialsProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 
 public class UsernamePasswordCredentials {
-  private static final Logger logger = LoggerFactory.getLogger(UsernamePasswordCredentials.class);
   public static final String USERNAME = "username";
   public static final String PASSWORD = "password";
 
@@ -58,7 +55,7 @@
       }
 
       Map<String, String> credentials = queryUser != null
-        ? credentialsProvider.getCredentials(queryUser)
+        ? credentialsProvider.getUserCredentials(queryUser)
         : credentialsProvider.getCredentials();
 
       if (credentials.size() == 0) {
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordWithProxyCredentials.java b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordWithProxyCredentials.java
index d6285a3..6a2d6b7 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordWithProxyCredentials.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/UsernamePasswordWithProxyCredentials.java
@@ -52,7 +52,7 @@
       }
 
       Map<String, String> credentials = queryUser != null
-        ? credentialsProvider.getCredentials(queryUser)
+        ? credentialsProvider.getUserCredentials(queryUser)
         : credentialsProvider.getCredentials();
 
       if (credentials.size() == 0) {
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java
index 66b2279..08217dc 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/oauth/OAuthTokenCredentials.java
@@ -70,7 +70,7 @@
       }
 
       Map<String, String> credentials = queryUser != null
-        ? credentialsProvider.getCredentials(queryUser)
+        ? credentialsProvider.getUserCredentials(queryUser)
         : credentialsProvider.getCredentials();
 
       if (credentials.size() == 0) {
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/vault/VaultCredentialsProvider.java b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/vault/VaultCredentialsProvider.java
index 8de6282..2df1e65 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/vault/VaultCredentialsProvider.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/store/security/vault/VaultCredentialsProvider.java
@@ -20,12 +20,17 @@
 import com.bettercloud.vault.Vault;
 import com.bettercloud.vault.VaultConfig;
 import com.bettercloud.vault.VaultException;
+import com.bettercloud.vault.response.AuthResponse;
+import com.bettercloud.vault.response.LogicalResponse;
 import com.fasterxml.jackson.annotation.JacksonInject;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.OptBoolean;
 import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.common.exceptions.UserException;
 import org.apache.drill.common.logical.security.CredentialsProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -37,15 +42,17 @@
  */
 public class VaultCredentialsProvider implements CredentialsProvider {
 
+  private static final Logger logger = LoggerFactory.getLogger(VaultCredentialsProvider.class);
   // Drill boot options used to configure a Vault credentials provider
   public static final String VAULT_ADDRESS = "drill.exec.storage.vault.address";
-  public static final String VAULT_TOKEN = "drill.exec.storage.vault.token";
+  public static final String VAULT_APP_ROLE_ID = "drill.exec.storage.vault.app_role_id";
+  public static final String VAULT_SECRET_ID = "drill.exec.storage.vault.secret_id";
+  public static final String QUERY_USER_VAR = "$user";
 
-  private final String secretPath;
-
+  private final String secretPath, appRoleId, secretId;
   private final Map<String, String> propertyNames;
-
-  private final Vault vault;
+  private final VaultConfig vaultConfig;
+  private Vault vault;
 
   /**
    * @param secretPath The Vault key value from which to read
@@ -58,36 +65,110 @@
       @JsonProperty("secretPath") String secretPath,
       @JsonProperty("propertyNames") Map<String, String> propertyNames,
       @JacksonInject(useInput = OptBoolean.FALSE) DrillConfig config) throws VaultException {
+
     this.propertyNames = propertyNames;
     this.secretPath = secretPath;
-    String vaultAddress = Objects.requireNonNull(config.getString(VAULT_ADDRESS),
-        String.format("Vault address is not specified. Please set [%s] config property.", VAULT_ADDRESS));
-    String token = Objects.requireNonNull(config.getString(VAULT_TOKEN),
-        String.format("Vault token is not specified. Please set [%s] config property.", VAULT_TOKEN));
+    this.appRoleId = Objects.requireNonNull(
+      config.getString(VAULT_APP_ROLE_ID),
+      String.format(
+        "Vault app role id is not specified. Please set [%s] config property.",
+        VAULT_APP_ROLE_ID
+      )
+    );
+    this.secretId = Objects.requireNonNull(
+      config.getString(VAULT_SECRET_ID),
+      String.format(
+        "Vault secret id is not specified. Please set [%s] config property.",
+        VAULT_SECRET_ID
+      )
+    );
+    String vaultAddress = Objects.requireNonNull(
+      config.getString(VAULT_ADDRESS),
+      String.format(
+        "Vault address is not specified. Please set [%s] config property.",
+        VAULT_ADDRESS
+      )
+    );
 
-    VaultConfig vaultConfig = new VaultConfig()
+    this.vaultConfig = new VaultConfig()
         .address(vaultAddress)
-        .token(token)
         .build();
+    // Initial unauthenticated Vault client, needed for the first auth() call.
     this.vault = new Vault(vaultConfig);
   }
 
+  private Map<String, String> extractCredentials(Map<String, String> vaultSecrets) {
+    Map<String, String> credentials = new HashMap<>();
+    for (Map.Entry<String, String> entry : propertyNames.entrySet()) {
+      String cred = vaultSecrets.get(entry.getValue());
+      if (cred != null) {
+        credentials.put(entry.getKey(), vaultSecrets.get(entry.getValue()));
+      }
+    }
+    return credentials;
+  }
+
+  private Map<String, String> getCredentialsAt(String path) {
+    LogicalResponse resp;
+    // Obtain this thread's own reference to the current Vault object to use
+    // for deciding whether _we_ need to reauthenticate in the event of an
+    // unauthorised read, or another thread has done that already.
+    Vault threadVault = this.vault;
+
+    try {
+      logger.debug("Attempting to fetch secrets from Vault path {}.", path);
+      resp = threadVault.logical().read(path);
+
+      if (resp.getRestResponse().getStatus() == 403) {
+        logger.info("Attempt to fetch secrets received HTTP 403 from Vault.");
+        synchronized (this) {
+          if (threadVault == vault) {
+            // The Vault object has not already been replaced by another thread,
+            // reauthenticate and replace it.
+            logger.info("Attempting to reauthenticate.");
+            AuthResponse authResp = vault.auth().loginByAppRole(appRoleId, secretId);
+            vault = new Vault(vaultConfig.token(authResp.getAuthClientToken()));
+          } else {
+            logger.debug("Another caller has already attempted reauthentication.");
+          }
+        }
+        logger.debug("Reattempting to fetch secrets from Vault path {}", path);
+        resp = vault.logical().read(path);
+      }
+      return extractCredentials(resp.getData());
+
+    } catch (VaultException ex) {
+      throw UserException.systemError(ex)
+        .message("Error while fetching credentials from vault")
+        .build(logger);
+    }
+  }
+
   @Override
   public Map<String, String> getCredentials() {
-    Map<String, String> credentials = new HashMap<>();
-    propertyNames.forEach((key, value) -> {
-      try {
-        String credValue = vault.logical()
-            .read(secretPath)
-            .getData()
-            .get(value);
-        credentials.put(key, credValue);
-      } catch (VaultException e) {
-        throw new RuntimeException("Error while fetching credentials from vault", e);
-      }
-    });
+    Map<String, String> creds = getCredentialsAt(secretPath);
+    if (creds.isEmpty()) {
+      logger.warn(
+        "No credentials matching the configured property names were readable at {}",
+        secretPath
+      );
+    }
+    return creds;
+  }
 
-    return credentials;
+  @Override
+  public Map<String, String> getUserCredentials(String queryUser) {
+    // Resolve a Vault path that may contain the $user var, e.g. /org/dept/$user -> /org/dept/alice
+    String resolvedPath = secretPath.replace(QUERY_USER_VAR, queryUser);
+    Map<String, String> creds = getCredentialsAt(resolvedPath);
+    if (creds.isEmpty()) {
+      logger.warn(
+        "No credentials for {} matching the configured property names were readable at {}",
+        queryUser,
+        resolvedPath
+      );
+    }
+    return creds;
   }
 
   public String getSecretPath() {
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestVaultUserAuthenticator.java b/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestVaultUserAuthenticator.java
index d5bf5cf..ad25217 100644
--- a/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestVaultUserAuthenticator.java
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestVaultUserAuthenticator.java
@@ -42,15 +42,15 @@
 @Category(SecurityTest.class)
 public class TestVaultUserAuthenticator extends ClusterTest {
 
-  private static final String ROOT_TOKEN_VALUE = "vault-token";
+  private static final String VAULT_ROOT_TOKEN = "vault-token";
 
   private static String vaultAddr;
 
   @ClassRule
   public static final VaultContainer<?> vaultContainer =
-      new VaultContainer<>(DockerImageName.parse("vault").withTag("1.1.3"))
+      new VaultContainer<>(DockerImageName.parse("vault").withTag("1.10.3"))
           .withLogLevel(VaultLogLevel.Debug)
-          .withVaultToken(ROOT_TOKEN_VALUE)
+          .withVaultToken(VAULT_ROOT_TOKEN)
           .withInitCommand(
             "auth enable userpass",
             "write auth/userpass/users/alice password=pass1 policies=admins",
@@ -93,7 +93,7 @@
     // Use the Vault client lib to obtain Vault tokens for our test users.
     VaultConfig vaultConfig = new VaultConfig()
       .address(vaultAddr)
-      .token(ROOT_TOKEN_VALUE)
+      .token(VAULT_ROOT_TOKEN)
       .build();
 
     Vault vault = new Vault(vaultConfig);
diff --git a/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderImplementationsTest.java b/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderImplementationsTest.java
index b8df40a..c49df89 100644
--- a/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderImplementationsTest.java
+++ b/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderImplementationsTest.java
@@ -17,7 +17,11 @@
  */
 package org.apache.drill.storage;
 
+import com.bettercloud.vault.Vault;
+import com.bettercloud.vault.VaultConfig;
 import com.bettercloud.vault.VaultException;
+import com.bettercloud.vault.response.LogicalResponse;
+
 import org.apache.drill.common.config.DrillConfig;
 import org.apache.drill.common.logical.security.CredentialsProvider;
 import org.apache.drill.exec.store.security.EnvCredentialsProvider;
@@ -31,6 +35,7 @@
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.testcontainers.containers.BindMode;
 import org.testcontainers.utility.DockerImageName;
 import org.testcontainers.vault.VaultContainer;
 
@@ -41,24 +46,62 @@
 
 public class CredentialsProviderImplementationsTest extends ClusterTest {
 
-  private static final String VAULT_TOKEN_VALUE = "vault-token";
-
-  private static final String SECRET_PATH = "secret/testing";
+  private static final String VAULT_ROOT_TOKEN = "vault-token";
+  private static final String VAULT_APP_ROLE_PATH = "auth/approle/role/drill-role";
+  private static final String SHARED_SECRET_PATH = "secret/testing";
+  private static final String USER_SECRET_PATH = "secret/testing/$user";
+  private static final String CONTAINER_POLICY_PATH = "/tmp/read-vault-secrets.hcl";
 
   @ClassRule
   public static final VaultContainer<?> vaultContainer =
-      new VaultContainer<>(DockerImageName.parse("vault").withTag("1.1.3"))
-          .withVaultToken(VAULT_TOKEN_VALUE)
-          .withVaultPort(8200)
-          .withSecretInVault(SECRET_PATH,
-              "top_secret=password1",
-              "db_password=dbpassword1");
+    new VaultContainer<>(DockerImageName.parse("vault").withTag("1.10.3"))
+      .withVaultToken(VAULT_ROOT_TOKEN)
+      .withSecretInVault(SHARED_SECRET_PATH,
+          "top_secret=password1",
+          "db_password=dbpassword1")
+      .withSecretInVault(USER_SECRET_PATH.replace(VaultCredentialsProvider.QUERY_USER_VAR, "alice"),
+          "top_secret=password1",
+          "db_password=dbpassword1")
+      .withClasspathResourceMapping("vault/read-vault-secrets.hcl", CONTAINER_POLICY_PATH, BindMode.READ_ONLY)
+      .withInitCommand(
+        "auth enable approle",
+        String.format("policy write read-secrets %s", CONTAINER_POLICY_PATH),
+        String.format("write %s policies=read-secrets", VAULT_APP_ROLE_PATH)
+      );
 
   @BeforeClass
   public static void init() throws Exception {
+    String vaultAddr = String.format(
+      "http://%s:%d",
+      vaultContainer.getHost(),
+      vaultContainer.getFirstMappedPort()
+    );
+
+    VaultConfig vaultConfig = new VaultConfig()
+      .address(vaultAddr)
+      .token(VAULT_ROOT_TOKEN)
+      .build();
+
+    // While other Vault paths in the test container seem to work fine with KV engine v2,
+    // the AppRole paths produced 404s and forced the specification of version 1 in the
+    // BetterCloud client used to perform AppRole operations.
+    Vault vault = new Vault(vaultConfig, 1);
+
+    LogicalResponse resp = vault.logical()
+      .read(String.format("%s/role-id", VAULT_APP_ROLE_PATH));
+    String appRoleId = resp.getData().get("role_id");
+
+    resp = vault.logical().write(
+      String.format("%s/secret-id", VAULT_APP_ROLE_PATH),
+      Collections.emptyMap()
+    );
+    String secretId = resp.getData().get("secret_id");
+
     startCluster(ClusterFixture.builder(dirTestWatcher)
-        .configProperty(VaultCredentialsProvider.VAULT_ADDRESS, "http://" + vaultContainer.getHost() + ":" + vaultContainer.getMappedPort(8200))
-    .configProperty(VaultCredentialsProvider.VAULT_TOKEN, VAULT_TOKEN_VALUE));
+      .configProperty(VaultCredentialsProvider.VAULT_ADDRESS, vaultAddr)
+      .configProperty(VaultCredentialsProvider.VAULT_APP_ROLE_ID, appRoleId)
+      .configProperty(VaultCredentialsProvider.VAULT_SECRET_ID, secretId)
+    );
   }
 
   @Test
@@ -102,16 +145,52 @@
   public void testVaultCredentialsProvider() throws VaultException {
     DrillConfig config = cluster.drillbit().getContext().getConfig();
 
-    CredentialsProvider envCredentialsProvider = new VaultCredentialsProvider(
-        SECRET_PATH,
+    CredentialsProvider vaultCredsProvider = new VaultCredentialsProvider(
+        SHARED_SECRET_PATH,
         ImmutableMap.of(UsernamePasswordCredentials.USERNAME, "top_secret",
             UsernamePasswordCredentials.PASSWORD, "db_password"),
         config);
 
-    Map<String, String> actualCredentials = envCredentialsProvider.getCredentials();
+    Map<String, String> actualCredentials = vaultCredsProvider.getCredentials();
 
     assertEquals(ImmutableMap.of(UsernamePasswordCredentials.USERNAME, "password1",
         UsernamePasswordCredentials.PASSWORD, "dbpassword1"),
         actualCredentials);
   }
+
+  @Test
+  public void testVaultUserCredentialsPresent() throws VaultException {
+    DrillConfig config = cluster.drillbit().getContext().getConfig();
+
+    CredentialsProvider vaultCredsProvider = new VaultCredentialsProvider(
+        USER_SECRET_PATH,
+        ImmutableMap.of(UsernamePasswordCredentials.USERNAME, "top_secret",
+            UsernamePasswordCredentials.PASSWORD, "db_password"),
+        config);
+
+    Map<String, String> actualCredentials = vaultCredsProvider.getUserCredentials("alice");
+
+    assertEquals(
+      ImmutableMap.of(
+        UsernamePasswordCredentials.USERNAME, "password1",
+        UsernamePasswordCredentials.PASSWORD, "dbpassword1"
+      ),
+      actualCredentials
+    );
+  }
+
+  @Test
+  public void testVaultUserCredentialsAbsent() throws VaultException {
+    DrillConfig config = cluster.drillbit().getContext().getConfig();
+
+    CredentialsProvider vaultCredsProvider = new VaultCredentialsProvider(
+        USER_SECRET_PATH,
+        ImmutableMap.of(UsernamePasswordCredentials.USERNAME, "top_secret",
+            UsernamePasswordCredentials.PASSWORD, "db_password"),
+        config);
+
+    Map<String, String> actualCredentials = vaultCredsProvider.getUserCredentials("bob");
+
+    assertEquals(Collections.<String, String>emptyMap(), actualCredentials);
+  }
 }
diff --git a/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderSerDeTest.java b/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderSerDeTest.java
index b254987..ab4946b 100644
--- a/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderSerDeTest.java
+++ b/exec/java-exec/src/test/java/org/apache/drill/storage/CredentialsProviderSerDeTest.java
@@ -31,32 +31,22 @@
 import org.apache.drill.test.ClusterFixture;
 import org.apache.drill.test.ClusterTest;
 import org.junit.BeforeClass;
-import org.junit.ClassRule;
 import org.junit.Test;
-import org.testcontainers.utility.DockerImageName;
-import org.testcontainers.vault.VaultContainer;
 
 import static org.junit.Assert.assertEquals;
 
 public class CredentialsProviderSerDeTest extends ClusterTest {
 
-  private static final String VAULT_TOKEN_VALUE = "vault-token";
-
   private static final String SECRET_PATH = "secret/testing";
 
-  @ClassRule
-  public static final VaultContainer<?> vaultContainer =
-      new VaultContainer<>(DockerImageName.parse("vault").withTag("1.1.3"))
-          .withVaultToken(VAULT_TOKEN_VALUE)
-          .withSecretInVault(SECRET_PATH,
-              "top_secret=password1",
-              "db_password=dbpassword1");
-
   @BeforeClass
   public static void init() throws Exception {
     startCluster(ClusterFixture.builder(dirTestWatcher)
-        .configProperty(VaultCredentialsProvider.VAULT_ADDRESS, "http://" + vaultContainer.getHost() + ":" + vaultContainer.getFirstMappedPort())
-        .configProperty(VaultCredentialsProvider.VAULT_TOKEN, VAULT_TOKEN_VALUE));
+      // Bogus Vault server values are sufficient for the tests in this class.
+      .configProperty(VaultCredentialsProvider.VAULT_ADDRESS, "foo")
+      .configProperty(VaultCredentialsProvider.VAULT_APP_ROLE_ID, "foo")
+      .configProperty(VaultCredentialsProvider.VAULT_SECRET_ID, "foo")
+    );
   }
 
   @Test
diff --git a/exec/java-exec/src/test/resources/vault/read-vault-secrets.hcl b/exec/java-exec/src/test/resources/vault/read-vault-secrets.hcl
new file mode 100644
index 0000000..62bbdd3
--- /dev/null
+++ b/exec/java-exec/src/test/resources/vault/read-vault-secrets.hcl
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+path "secret/*"
+{
+  capabilities = ["read"]
+}
diff --git a/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java b/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
index ba38ad0..0992579 100644
--- a/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
+++ b/logical/src/main/java/org/apache/drill/common/logical/security/CredentialsProvider.java
@@ -46,7 +46,7 @@
    * @return A Map of the logged in user's credentials.
    */
   @JsonIgnore
-  default Map<String, String> getCredentials(String username) {
+  default Map<String, String> getUserCredentials(String username) {
     throw UserException.unsupportedError()
       .message("%s does not support per-user credentials.", getClass())
       .build(logger);
diff --git a/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java b/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
index 5624b39..678b086 100644
--- a/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
+++ b/logical/src/main/java/org/apache/drill/common/logical/security/PlainCredentialsProvider.java
@@ -68,7 +68,8 @@
 
   @Override
   @JsonIgnore(false)
-  @JsonProperty("credentials") public Map<String, String> getCredentials() {
+  @JsonProperty("credentials")
+  public Map<String, String> getCredentials() {
     return credentials;
   }
 
@@ -85,7 +86,7 @@
    * @return A Map of the active user's credentials
    */
   @Override
-  public Map<String, String> getCredentials(String queryUser) {
+  public Map<String, String> getUserCredentials(String queryUser) {
     assert queryUser != null;
     logger.debug("Getting credentials for query user {}", queryUser);