This is an automated email from the ASF dual-hosted git repository.

cgivre pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/drill.git


The following commit(s) were added to refs/heads/master by this push:
     new fe1d15f  DRILL-8047: Add a custom authn provider for HashiCorp Vault 
(#2394)
fe1d15f is described below

commit fe1d15f1941334792e3f1c1dd53d932dd1b66279
Author: James Turton <[email protected]>
AuthorDate: Fri Dec 10 00:59:32 2021 +0200

    DRILL-8047: Add a custom authn provider for HashiCorp Vault (#2394)
    
    * Add Vault user authenticator and unit tests.
    
    * Fix test failures.
    
    * Enable Vault token auth method.
    
    * Fix bug affecting token owner check.
    
    * Add Vault token auth method unit test.
    
    * Get token auth unit test passing.
---
 .../rpc/user/security/VaultUserAuthenticator.java  | 228 +++++++++++++++++++++
 .../server/rest/auth/DrillRestLoginService.java    |   2 +-
 .../security/vault/VaultCredentialsProvider.java   |   1 +
 .../TestHtpasswdFileUserAuthenticator.java         |   3 +
 .../user/security/TestVaultUserAuthenticator.java  | 157 ++++++++++++++
 5 files changed, 390 insertions(+), 1 deletion(-)

diff --git 
a/exec/java-exec/src/main/java/org/apache/drill/exec/rpc/user/security/VaultUserAuthenticator.java
 
b/exec/java-exec/src/main/java/org/apache/drill/exec/rpc/user/security/VaultUserAuthenticator.java
new file mode 100644
index 0000000..110d5f7
--- /dev/null
+++ 
b/exec/java-exec/src/main/java/org/apache/drill/exec/rpc/user/security/VaultUserAuthenticator.java
@@ -0,0 +1,228 @@
+/*
+ * 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.
+ */
+package org.apache.drill.exec.rpc.user.security;
+
+import com.bettercloud.vault.response.LookupResponse;
+import com.bettercloud.vault.Vault;
+import com.bettercloud.vault.VaultConfig;
+import com.bettercloud.vault.VaultException;
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.exec.exception.DrillbitStartupException;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Implement {@link org.apache.drill.exec.rpc.user.security.UserAuthenticator}
+ * based on HashiCorp Vault.  Configure the Vault client using the Drill BOOT
+ * options that appear below.
+ */
+@Slf4j
+@EqualsAndHashCode
+@UserAuthenticatorTemplate(type = "vault")
+public class VaultUserAuthenticator implements UserAuthenticator {
+
+  // Drill boot options used to configure Vault auth.
+  public static final String VAULT_ADDRESS = 
"drill.exec.security.user.auth.vault.address";
+  public static final String VAULT_TOKEN = 
"drill.exec.security.user.auth.vault.token";
+  public static final String VAULT_AUTH_METHOD = 
"drill.exec.security.user.auth.vault.method";
+
+  // The subset of Vault auth methods that are supported by this authenticator
+  public enum VaultAuthMethod {
+    APP_ROLE,
+    LDAP,
+    USER_PASS,
+    VAULT_TOKEN
+  }
+
+  private VaultConfig vaultConfig;
+
+  private Vault vault;
+
+  private VaultAuthMethod authMethod;
+
+  /**
+   * Reads Drill BOOT options and uses them to set up a Vault client.
+   * @param config object providing Drill config settings for Vault
+   * @throws DrillbitStartupException if the provided Vault configuration is 
invalid
+   */
+  @Override
+  public void setup(DrillConfig config) throws DrillbitStartupException {
+    // Read config values
+    String vaultAddress = Objects.requireNonNull(
+      config.getString(VAULT_ADDRESS),
+      String.format(
+        "Vault address BOOT option is not specified. Please set [%s] config " +
+        "option.",
+        VAULT_ADDRESS
+      )
+    );
+
+    this.authMethod = VaultAuthMethod.valueOf(
+      Objects.requireNonNull(
+        config.getString(VAULT_AUTH_METHOD),
+        String.format(
+          "Vault auth method is not specified. Please set [%s] config option.",
+          VAULT_AUTH_METHOD
+        )
+      )
+    );
+
+    VaultConfig vaultConfBuilder = new VaultConfig().address(vaultAddress);
+    String vaultToken = config.hasPath(VAULT_TOKEN)
+      ? config.getString(VAULT_TOKEN)
+      : null;
+
+    if (this.authMethod == VaultAuthMethod.VAULT_TOKEN) {
+      // Drill will use end users' Vault tokens for Vault operations
+      if (vaultToken != null) {
+        logger.warn(
+          "When Drill is set to authenticate using end user Vault tokens the " 
+
+          "[{}] BOOT option is ignored.",
+          VAULT_TOKEN
+        );
+      }
+    } else {
+      // Drill needs its own Vault token set in a BOOT option
+      if (vaultToken == null) {
+        throw new DrillbitStartupException(String.format(
+          "Only the %s method is supported if you do not set a token in the " +
+            "[%s] config option.",
+          VaultAuthMethod.VAULT_TOKEN,
+          VAULT_TOKEN
+        ));
+      }
+      vaultConfBuilder.token(vaultToken);
+    }
+
+    // Initialise Vault client
+    try {
+      logger.debug(
+        "Tries to init a Vault client with Vault addr = {}, auth method = {}",
+        vaultAddress,
+        authMethod
+      );
+
+      this.vaultConfig = vaultConfBuilder.build();
+      this.vault = new Vault(this.vaultConfig);
+    } catch (VaultException e) {
+      logger.error(String.join(System.lineSeparator(),
+          "Error initialising the Vault client library using configuration: ",
+          "\tvaultAddress: {}",
+          "\tvaultToken: {}",
+          "\tauthMethod: {}"
+        ),
+        vaultAddress,
+        vaultToken,
+        authMethod,
+        e
+      );
+      throw new DrillbitStartupException(
+        "Error initialising the Vault client library: " + e.getMessage(),
+        e
+      );
+    }
+  }
+
+  /**
+   * Attempts to authenticate a Drill user with the provided password and the
+   * configured Vault auth method.  Only auth methods that have a natural
+   * mapping to PLAIN authentication's user and password strings are supported.
+   * @param user username
+   * @param password password
+   * @throws UserAuthenticationException if the authentication attempt fails
+   */
+  @Override
+  public void authenticate(
+    String user,
+    String password
+  ) throws UserAuthenticationException {
+
+    try {
+      logger.debug("Tries to authenticate user {} using {}", user, authMethod);
+
+      switch (authMethod) {
+        case APP_ROLE:
+          // user = role id, password = secret id
+          vault.auth().loginByAppRole(user, password);
+          break;
+        case LDAP:
+          // user = username, password = password
+          vault.auth().loginByLDAP(user, password);
+          break;
+        case USER_PASS:
+          // user = username, password = password
+          vault.auth().loginByUserPass(user, password);
+          break;
+        case VAULT_TOKEN:
+          // user = username, password = vault token
+
+          // Create a throwaway Vault client using the provided token and send 
a
+          // token lookup request to Vault which will fail if the token is
+          // invalid.
+          VaultConfig lookupConfig = new VaultConfig()
+            .address(this.vaultConfig.getAddress())
+            .token(password)
+            .build();
+
+          LookupResponse lookupResp = new 
Vault(lookupConfig).auth().lookupSelf();
+          // Check token owner using getPath() because getUsername() sometimes
+          // returns null.
+          if (!lookupResp.getPath().endsWith("/" + user)) {
+            throw new UserAuthenticationException(String.format(
+              "Attempted to authenticate user %s with a Vault token that is " +
+              " valid but has path %s!",
+              user,
+              lookupResp.getPath()
+            ));
+          }
+          break;
+
+        default:
+          throw new UserAuthenticationException(String.format(
+            "The Vault authentication method '%s' is not supported",
+            authMethod
+          ));
+        }
+    } catch (VaultException e) {
+      logger.warn("Failed to authenticate user {} using {}: {}.", user, 
authMethod, e);
+      throw new UserAuthenticationException(
+        String.format(
+          "Failed to authenticate user %s using %s: %s",
+          user,
+          authMethod,
+          e.getMessage()
+        )
+      );
+    }
+
+    logger.info(
+      "User {} authenticated against Vault successfully.",
+      user
+    );
+  }
+
+  @Override
+  public void close() throws IOException {
+    this.vault = null;
+    logger.debug("Has been closed.");
+  }
+}
diff --git 
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillRestLoginService.java
 
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillRestLoginService.java
index a21a0f1..6f3b969 100644
--- 
a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillRestLoginService.java
+++ 
b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillRestLoginService.java
@@ -104,7 +104,7 @@ public class DrillRestLoginService implements LoginService {
       if (e instanceof UserAuthenticationException) {
         logger.debug("Authentication failed for WebUser '{}'", username, e);
       } else {
-        logger.error("UnExpected failure occurred for WebUser {} during 
login.", username, e);
+        logger.error("Unexpected failure occurred for WebUser {} during 
login.", username, e);
       }
       return null;
     }
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 aff2acc..8de6282 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
@@ -37,6 +37,7 @@ import java.util.Objects;
  */
 public class VaultCredentialsProvider implements CredentialsProvider {
 
+  // 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";
 
diff --git 
a/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestHtpasswdFileUserAuthenticator.java
 
b/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestHtpasswdFileUserAuthenticator.java
index 9c27408..6b6b3ca 100644
--- 
a/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestHtpasswdFileUserAuthenticator.java
+++ 
b/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestHtpasswdFileUserAuthenticator.java
@@ -17,12 +17,14 @@
  */
 package org.apache.drill.exec.rpc.user.security;
 
+import org.apache.drill.categories.SecurityTest;
 import org.apache.drill.common.config.DrillProperties;
 import org.apache.drill.exec.ExecConstants;
 import org.apache.drill.test.ClientFixture;
 import org.apache.drill.test.ClusterFixture;
 import org.apache.drill.test.ClusterTest;
 import org.junit.Test;
+import org.junit.experimental.categories.Category;
 
 import java.io.File;
 import java.io.IOException;
@@ -33,6 +35,7 @@ import java.util.List;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+@Category(SecurityTest.class)
 public class TestHtpasswdFileUserAuthenticator extends ClusterTest {
   private File tempPasswdFile;
 
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
new file mode 100644
index 0000000..d5e33c2
--- /dev/null
+++ 
b/exec/java-exec/src/test/java/org/apache/drill/exec/rpc/user/security/TestVaultUserAuthenticator.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+package org.apache.drill.exec.rpc.user.security;
+
+import com.bettercloud.vault.Vault;
+import com.bettercloud.vault.VaultConfig;
+import com.bettercloud.vault.response.AuthResponse;
+import org.apache.drill.categories.SecurityTest;
+import org.apache.drill.common.config.DrillProperties;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.test.ClientFixture;
+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.junit.experimental.categories.Category;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.vault.VaultContainer;
+import org.testcontainers.vault.VaultLogLevel;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.fail;
+
+@Category(SecurityTest.class)
+public class TestVaultUserAuthenticator extends ClusterTest {
+
+  private static final String ROOT_TOKEN_VALUE = "vault-token";
+
+  private static String vaultAddr;
+
+  @ClassRule
+  public static final VaultContainer<?> vaultContainer =
+      new VaultContainer<>(DockerImageName.parse("vault").withTag("1.1.3"))
+          .withLogLevel(VaultLogLevel.Debug)
+          .withVaultToken(ROOT_TOKEN_VALUE)
+          .withInitCommand(
+            "auth enable userpass",
+            "write auth/userpass/users/alice password=pass1 policies=admins",
+            "write auth/userpass/users/bob password=buzzkill policies=admins"
+          );
+
+  @BeforeClass
+  public static void init() throws Exception {
+    vaultAddr = String.format(
+      "http://%s:%d";,
+      vaultContainer.getHost(),
+      vaultContainer.getMappedPort(8200)
+    );
+  }
+
+  @Test
+  public void testUserPassAuth() throws Exception {
+    cluster = ClusterFixture.bareBuilder(dirTestWatcher)
+      .clusterSize(3)
+      .configProperty(ExecConstants.ALLOW_LOOPBACK_ADDRESS_BINDING, true)
+      .configProperty(ExecConstants.USER_AUTHENTICATION_ENABLED, true)
+      .configProperty(ExecConstants.USER_AUTHENTICATOR_IMPL, "vault")
+      .configProperty(VaultUserAuthenticator.VAULT_ADDRESS, vaultAddr)
+      .configProperty(VaultUserAuthenticator.VAULT_TOKEN, ROOT_TOKEN_VALUE)
+      .configProperty(
+        VaultUserAuthenticator.VAULT_AUTH_METHOD,
+        VaultUserAuthenticator.VaultAuthMethod.USER_PASS
+      )
+      .build();
+
+    tryCredentials("notalice", "pass1", cluster, false);
+    tryCredentials("notbob", "buzzkill", cluster, false);
+    tryCredentials("alice", "wrong", cluster, false);
+    tryCredentials("bob", "incorrect", cluster, false);
+    tryCredentials("alice", "pass1", cluster, true);
+    tryCredentials("bob", "buzzkill", cluster, true);
+  }
+
+  @Test
+  public void testVaultTokenAuth() throws Exception {
+    // Use the Vault client lib to obtain Vault tokens for our test users.
+    VaultConfig vaultConfig = new VaultConfig()
+      .address(vaultAddr)
+      .token(ROOT_TOKEN_VALUE)
+      .build();
+
+    Vault vault = new Vault(vaultConfig);
+    AuthResponse aliceResp = vault.auth().loginByUserPass("alice", "pass1");
+    String aliceAuthToken = aliceResp.getAuthClientToken();
+    AuthResponse bobResp = vault.auth().loginByUserPass("bob", "buzzkill");
+    String bobAuthToken = bobResp.getAuthClientToken();
+
+    // set up a new cluster with a config option selecting Vault token auth
+    cluster = ClusterFixture.bareBuilder(dirTestWatcher)
+      .clusterSize(3)
+      .configProperty(ExecConstants.ALLOW_LOOPBACK_ADDRESS_BINDING, true)
+      .configProperty(ExecConstants.USER_AUTHENTICATION_ENABLED, true)
+      .configProperty(ExecConstants.USER_AUTHENTICATOR_IMPL, "vault")
+      .configProperty(VaultUserAuthenticator.VAULT_ADDRESS, vaultAddr)
+      // test without any VAULT_TOKEN boot option for token auth method
+      .configProperty(
+        VaultUserAuthenticator.VAULT_AUTH_METHOD,
+        VaultUserAuthenticator.VaultAuthMethod.VAULT_TOKEN
+      )
+      .build();
+
+    tryCredentials("notalice", aliceAuthToken, cluster, false);
+    tryCredentials("notbob", bobAuthToken, cluster, false);
+    tryCredentials("alice", "wrong", cluster, false);
+    tryCredentials("bob", "incorrect", cluster, false);
+    tryCredentials("alice", aliceAuthToken, cluster, true);
+    tryCredentials("bob", bobAuthToken, cluster, true);
+  }
+
+  private static void tryCredentials(String user, String password, 
ClusterFixture cluster, boolean shouldSucceed) throws Exception {
+    try {
+      ClientFixture client = cluster.clientBuilder()
+        .property(DrillProperties.USER, user)
+        .property(DrillProperties.PASSWORD, password)
+        .build();
+
+      // Run few queries using the new client
+      List<String> queries = Arrays.asList(
+        "SHOW SCHEMAS",
+        "USE INFORMATION_SCHEMA",
+        "SHOW TABLES",
+        "SELECT * FROM INFORMATION_SCHEMA.`TABLES` WHERE TABLE_NAME LIKE 
'COLUMNS'",
+        "SELECT * FROM cp.`region.json` LIMIT 5");
+
+      for (String query : queries) {
+        client.queryBuilder().sql(query).run();
+      }
+
+      if (!shouldSucceed) {
+        fail("Expected connect to fail because of incorrect username / 
password combination, but it succeeded");
+      }
+    } catch (IllegalStateException e) {
+      if (shouldSucceed) {
+        throw e;
+      }
+    }
+  }
+
+}

Reply via email to