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

mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git


The following commit(s) were added to refs/heads/main by this push:
     new 4acf635f3 [CALCITE-6135] BEARER authentication support
4acf635f3 is described below

commit 4acf635f365d31b94556f3f1b731375347282267
Author: Richard Antal <[email protected]>
AuthorDate: Mon Feb 9 13:19:41 2026 +0100

    [CALCITE-6135] BEARER authentication support
    
    Co-authored-by: Aron Meszaros <[email protected]>
---
 bom/build.gradle.kts                               |   1 +
 core/build.gradle.kts                              |   1 +
 .../calcite/avatica/BuiltInConnectionProperty.java |  13 +-
 .../apache/calcite/avatica/ConnectionConfig.java   |   8 +
 .../calcite/avatica/ConnectionConfigImpl.java      |  18 +-
 .../apache/calcite/avatica/ConnectionProperty.java |   2 +-
 .../calcite/avatica/ConnectionPropertyValue.java   | 113 ++++++++++++
 .../calcite/avatica/remote/AuthenticationType.java |   1 +
 .../remote/AvaticaCommonsHttpClientImpl.java       |  35 +++-
 .../remote/AvaticaHttpClientFactoryImpl.java       |  18 ++
 ...cationType.java => BearerAuthenticateable.java} |  19 +-
 ...nticationType.java => BearerTokenProvider.java} |  29 ++-
 .../avatica/remote/BearerTokenProviderFactory.java |  62 +++++++
 ...nType.java => ConstantBearerTokenProvider.java} |  33 ++--
 .../remote/BearerTokenProviderFactoryTest.java     | 204 +++++++++++++++++++++
 .../remote/ConstantBearerTokenProviderTest.java    |  57 ++++++
 gradle.properties                                  |   1 +
 17 files changed, 581 insertions(+), 34 deletions(-)

diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index 6f46ab9a6..1eb8a98ee 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -79,6 +79,7 @@ dependencies {
         apiv("org.ow2.asm:asm-tree", "asm")
         apiv("org.ow2.asm:asm-util", "asm")
         apiv("org.slf4j:slf4j-api", "slf4j")
+        apiv("commons-io:commons-io")
         // The log4j2 binding should be a runtime dependency but given that
         // some modules shade this dependency we need to keep it as api
         apiv("org.apache.logging.log4j:log4j-slf4j-impl", "log4j2")
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 12bdb490a..ffa8ecc6d 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -44,6 +44,7 @@ dependencies {
     testImplementation("org.mockito:mockito-core")
     testImplementation("org.mockito:mockito-inline")
     testImplementation("org.hamcrest:hamcrest-core")
+    testImplementation("commons-io:commons-io")
     testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl")
 }
 
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java 
b/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
index 56112045e..00034cbd5 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java
@@ -137,7 +137,18 @@ public enum BuiltInConnectionProperty implements 
ConnectionProperty {
    * HTTP Response Timeout (socket timeout) in milliseconds.
    */
   HTTP_RESPONSE_TIMEOUT("http_response_timeout",
-      Type.NUMBER, Timeout.ofMinutes(3).toMilliseconds(), false);
+                        Type.NUMBER, Timeout.ofMinutes(3).toMilliseconds(), 
false),
+
+  /** Bearer token to use to perform Bearer authentication. */
+  BEARER_TOKEN("bearer_token", Type.STRING, null, false),
+
+  /**
+   * Path to a file that contains bearer token(s) to perform Bearer 
authentication.
+   */
+  TOKEN_FILE("token_file", Type.STRING, "", false),
+
+  /** Classname of the BearerTokenProvider. */
+  TOKEN_PROVIDER_CLASS("bearer_token_provider_class", Type.STRING, null, 
false);
 
   private final String camelName;
   private final Type type;
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java 
b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
index 69b98f54d..bdf36041d 100644
--- a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
+++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java
@@ -84,6 +84,14 @@ public interface ConnectionConfig {
   long getHttpConnectionTimeout();
   /** @see BuiltInConnectionProperty#HTTP_RESPONSE_TIMEOUT **/
   long getHttpResponseTimeout();
+  /** @see BuiltInConnectionProperty#TOKEN_FILE */
+  String getTokenFile();
+  /** @see BuiltInConnectionProperty#BEARER_TOKEN */
+  String getBearerToken();
+  /** @see BuiltInConnectionProperty#TOKEN_PROVIDER_CLASS */
+  String getBearerTokenProviderClass();
+
+  ConnectionPropertyValue customPropertyValue(ConnectionProperty property);
 }
 
 // End ConnectionConfig.java
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java 
b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
index 6326de55d..fd4728acb 100644
--- a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
+++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java
@@ -175,6 +175,22 @@ public long getHttpResponseTimeout() {
     return 
BuiltInConnectionProperty.HTTP_RESPONSE_TIMEOUT.wrap(properties).getLong();
   }
 
+  public String getTokenFile() {
+    return BuiltInConnectionProperty.TOKEN_FILE.wrap(properties).getString();
+  }
+
+  public String getBearerToken() {
+    return BuiltInConnectionProperty.BEARER_TOKEN.wrap(properties).getString();
+  }
+
+  public String getBearerTokenProviderClass() {
+    return 
BuiltInConnectionProperty.TOKEN_PROVIDER_CLASS.wrap(properties).getString();
+  }
+
+  public ConnectionPropertyValue customPropertyValue(ConnectionProperty 
property) {
+    return property.wrap(properties);
+  }
+
   /** Converts a {@link Properties} object containing (name, value)
    * pairs into a map whose keys are
    * {@link org.apache.calcite.avatica.InternalProperty} objects.
@@ -204,7 +220,7 @@ public static Map<ConnectionProperty, String> 
parse(Properties properties,
   }
 
   /** The combination of a property definition and a map of property values. */
-  public static class PropEnv {
+  public static class PropEnv implements ConnectionPropertyValue {
     final Map<? extends ConnectionProperty, String> map;
     private final ConnectionProperty property;
 
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/ConnectionProperty.java 
b/core/src/main/java/org/apache/calcite/avatica/ConnectionProperty.java
index b41b9a31a..f0245bfa2 100644
--- a/core/src/main/java/org/apache/calcite/avatica/ConnectionProperty.java
+++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionProperty.java
@@ -40,7 +40,7 @@ public interface ConnectionProperty {
 
   /** Wraps this property with a properties object from which its value can be
    * obtained when needed. */
-  ConnectionConfigImpl.PropEnv wrap(Properties properties);
+  ConnectionPropertyValue wrap(Properties properties);
 
   /** Whether the property is mandatory. */
   boolean required();
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/ConnectionPropertyValue.java 
b/core/src/main/java/org/apache/calcite/avatica/ConnectionPropertyValue.java
new file mode 100644
index 000000000..888b00323
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionPropertyValue.java
@@ -0,0 +1,113 @@
+/*
+ * 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.calcite.avatica;
+
+public interface ConnectionPropertyValue {
+  /**
+   * Returns the string value of this property, or null if not specified and
+   * no default.
+   */
+  String getString();
+
+  /**
+   * Returns the string value of this property, or null if not specified and
+   * no default.
+   */
+  String getString(String defaultValue);
+
+  /**
+   * Returns the int value of this property. Throws if not set and no
+   * default.
+   */
+  int getInt();
+
+  /**
+   * Returns the int value of this property. Throws if not set and no
+   * default.
+   */
+  int getInt(Number defaultValue);
+
+  /**
+   * Returns the long value of this property. Throws if not set and no
+   * default.
+   */
+  long getLong();
+
+  /**
+   * Returns the long value of this property. Throws if not set and no
+   * default.
+   */
+  long getLong(Number defaultValue);
+
+  /**
+   * Returns the double value of this property. Throws if not set and no
+   * default.
+   */
+  double getDouble();
+
+  /**
+   * Returns the double value of this property. Throws if not set and no
+   * default.
+   */
+  double getDouble(Number defaultValue);
+
+  /**
+   * Returns the boolean value of this property. Throws if not set and no
+   * default.
+   */
+  boolean getBoolean();
+
+  /**
+   * Returns the boolean value of this property. Throws if not set and no
+   * default.
+   */
+  boolean getBoolean(boolean defaultValue);
+
+  /**
+   * Returns the enum value of this property. Throws if not set and no
+   * default.
+   */
+  <E extends Enum<E>> E getEnum(Class<E> enumClass);
+
+  /**
+   * Returns the enum value of this property. Throws if not set and no
+   * default.
+   */
+  <E extends Enum<E>> E getEnum(Class<E> enumClass, E defaultValue);
+
+  /**
+   * Returns an instance of a plugin.
+   *
+   * <p>Throws if not set and no default.
+   * Also throws if the class does not implement the required interface,
+   * or if it does not have a public default constructor or an public static
+   * field called {@code #INSTANCE}.
+   */
+  <T> T getPlugin(Class<T> pluginClass, T defaultInstance);
+
+  /**
+   * Returns an instance of a plugin, using a given class name if none is
+   * set.
+   *
+   * <p>Throws if not set and no default.
+   * Also throws if the class does not implement the required interface,
+   * or if it does not have a public default constructor or an public static
+   * field called {@code #INSTANCE}.
+   */
+  <T> T getPlugin(Class<T> pluginClass, String defaultClassName,
+                  T defaultInstance);
+}
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
index f483be9bd..8bdb7709a 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
@@ -24,6 +24,7 @@ public enum AuthenticationType {
   BASIC,
   DIGEST,
   SPNEGO,
+  BEARER,
   CUSTOM;
 }
 
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java
 
b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java
index 18a7e32cf..7c092bcdb 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaCommonsHttpClientImpl.java
@@ -22,6 +22,7 @@
 import org.apache.hc.client5.http.SystemDefaultDnsResolver;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.BearerToken;
 import org.apache.hc.client5.http.auth.Credentials;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
@@ -31,6 +32,7 @@
 import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -68,7 +70,7 @@
  * sent and received across the wire.
  */
 public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, 
HttpClientPoolConfigurable,
-    UsernamePasswordAuthenticateable, GSSAuthenticateable {
+    UsernamePasswordAuthenticateable, GSSAuthenticateable, 
BearerAuthenticateable {
   private static final Logger LOG = 
LoggerFactory.getLogger(AvaticaCommonsHttpClientImpl.class);
 
   // SPNEGO specific settings
@@ -152,6 +154,9 @@ private RequestConfig createRequestConfig() {
       if (authRegistry.lookup(StandardAuthScheme.SPNEGO) != null) {
         preferredSchemes.add(StandardAuthScheme.SPNEGO);
       }
+      if (authRegistry.lookup(StandardAuthScheme.BEARER) != null) {
+        preferredSchemes.add(StandardAuthScheme.BEARER);
+      }
       requestConfigBuilder.setTargetPreferredAuthSchemes(preferredSchemes);
       requestConfigBuilder.setProxyPreferredAuthSchemes(preferredSchemes);
     }
@@ -258,10 +263,36 @@ ClassicHttpResponse executeOpen(HttpHost httpHost, 
HttpPost post, HttpClientCont
     context.setRequestConfig(createRequestConfig());
   }
 
+  @Override public void setTokenProvider(String username, BearerTokenProvider 
tokenProvider) {
+    this.credentialsProvider = new BasicCredentialsProvider();
+    BearerToken bearerToken = null;
+    try {
+      bearerToken = new 
BearerToken(tokenProvider.obtain(Objects.requireNonNull(username)));
+    } catch (NullPointerException exception) {
+      LOG.warn("Failed to create BearerToken for the user: " + username, 
exception);
+    }
+    if (bearerToken != null) {
+      ((BasicCredentialsProvider) this.credentialsProvider)
+          .setCredentials(anyAuthScope, bearerToken);
+    } else {
+      // User does not have bearerToken
+      ((BasicCredentialsProvider) this.credentialsProvider)
+          .setCredentials(anyAuthScope, EmptyCredentials.INSTANCE);
+    }
+
+    this.authRegistry = RegistryBuilder.<AuthSchemeFactory>create()
+        .register(StandardAuthScheme.BEARER,
+            new BearerSchemeFactory())
+        .build();
+    context.setCredentialsProvider(credentialsProvider);
+    context.setAuthSchemeRegistry(authRegistry);
+    context.setRequestConfig(createRequestConfig());
+  }
+
   /**
    * A credentials implementation which returns null.
    */
-  private static class EmptyCredentials implements Credentials {
+  static class EmptyCredentials implements Credentials {
     public static final EmptyCredentials INSTANCE = new EmptyCredentials();
 
     @Deprecated
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
 
b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
index e8fcb77dc..445848979 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/AvaticaHttpClientFactoryImpl.java
@@ -131,6 +131,24 @@ public static AvaticaHttpClientFactoryImpl getInstance() {
       LOG.debug("{} is not capable of kerberos authentication.", authType);
     }
 
+    if (client instanceof BearerAuthenticateable) {
+      if (AuthenticationType.BEARER == authType) {
+        try {
+          BearerTokenProvider tokenProvider =
+                  BearerTokenProviderFactory.getBearerTokenProvider(config);
+          String username = config.avaticaUser();
+          if (null == username) {
+            username = System.getProperty("user.name");
+          }
+          ((BearerAuthenticateable) client).setTokenProvider(username, 
tokenProvider);
+        } catch (java.io.IOException e) {
+          LOG.debug("Failed to initialize bearer authentication");
+        }
+      }
+    } else {
+      LOG.debug("{} is not capable of bearer authentication.", authType);
+    }
+
     if (null != kerberosUtil) {
       client = new DoAsAvaticaHttpClient(client, kerberosUtil);
     }
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java
similarity index 69%
copy from 
core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
copy to 
core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java
index f483be9bd..8d7f53576 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java
@@ -17,14 +17,15 @@
 package org.apache.calcite.avatica.remote;
 
 /**
- * An enumeration for support types of authentication for the HttpServer.
+ * Interface that allows configuration of a username and BearerTokenProvider 
HTTP authentication.
  */
-public enum AuthenticationType {
-  NONE,
-  BASIC,
-  DIGEST,
-  SPNEGO,
-  CUSTOM;
-}
+public interface BearerAuthenticateable {
 
-// End AuthenticationType.java
+  /**
+   * Sets the username, tokenProvider to be used for authentication.
+   *
+   * @param username Username
+   * @param tokenProvider Bearer Token Provider
+   */
+  void setTokenProvider(String username, BearerTokenProvider tokenProvider);
+}
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java
similarity index 62%
copy from 
core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
copy to 
core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java
index f483be9bd..994f18ac0 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java
@@ -16,15 +16,26 @@
  */
 package org.apache.calcite.avatica.remote;
 
+import org.apache.calcite.avatica.ConnectionConfig;
+
+import java.io.IOException;
+
 /**
- * An enumeration for support types of authentication for the HttpServer.
+ * Interface that provides bearer token for authentication.
  */
-public enum AuthenticationType {
-  NONE,
-  BASIC,
-  DIGEST,
-  SPNEGO,
-  CUSTOM;
-}
+public interface BearerTokenProvider {
 
-// End AuthenticationType.java
+  /**
+   * Initialize JSON Web Token from the config to be used for authentication.
+   *
+   * @param config ConnectionConfig
+   */
+  void init(ConnectionConfig config) throws IOException;
+
+  /**
+   * Returns JSON Web Token used for authentication or null.
+   *
+   * @param username Username
+   */
+  String obtain(String username);
+}
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactory.java
 
b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactory.java
new file mode 100644
index 000000000..f6dda63b7
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactory.java
@@ -0,0 +1,62 @@
+/*
+ * 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.calcite.avatica.remote;
+
+import org.apache.calcite.avatica.ConnectionConfig;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+
+public class BearerTokenProviderFactory {
+  public static final String TOKEN_PROVIDER_IMPL_DEFAULT =
+          ConstantBearerTokenProvider.class.getName();
+
+  private BearerTokenProviderFactory() {}
+
+  public static BearerTokenProvider getBearerTokenProvider(ConnectionConfig 
config)
+          throws IOException {
+    String tokenProviderClassName = config.getBearerTokenProviderClass();
+    if (null == tokenProviderClassName) {
+      tokenProviderClassName = TOKEN_PROVIDER_IMPL_DEFAULT;
+    }
+    BearerTokenProvider tokenProvider = 
instantiateTokenProvider(tokenProviderClassName);
+    tokenProvider.init(config);
+    return tokenProvider;
+  }
+
+  private static BearerTokenProvider instantiateTokenProvider(String 
className) {
+    BearerTokenProvider tokenProvider = null;
+    Exception tokenProviderCreationException = null;
+
+    try {
+      Class<? extends BearerTokenProvider> clz =
+              Class.forName(className).asSubclass(BearerTokenProvider.class);
+      Constructor<? extends BearerTokenProvider> constructor = 
clz.getConstructor();
+      tokenProvider = constructor.newInstance();
+    } catch (Exception e) {
+      tokenProviderCreationException = e;
+    }
+
+    if (tokenProvider == null) {
+      throw new RuntimeException("Failed to construct BearerTokenProvider 
implementation "
+              + className, tokenProviderCreationException);
+    } else {
+      return tokenProvider;
+    }
+  }
+
+}
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProvider.java
similarity index 55%
copy from 
core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
copy to 
core/src/main/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProvider.java
index f483be9bd..4a350c192 100644
--- 
a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java
+++ 
b/core/src/main/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProvider.java
@@ -16,15 +16,26 @@
  */
 package org.apache.calcite.avatica.remote;
 
-/**
- * An enumeration for support types of authentication for the HttpServer.
- */
-public enum AuthenticationType {
-  NONE,
-  BASIC,
-  DIGEST,
-  SPNEGO,
-  CUSTOM;
-}
+import org.apache.calcite.avatica.BuiltInConnectionProperty;
+import org.apache.calcite.avatica.ConnectionConfig;
+
+import java.io.IOException;
 
-// End AuthenticationType.java
+public class ConstantBearerTokenProvider implements BearerTokenProvider {
+  private String token;
+
+  @Override
+  public void init(ConnectionConfig config) throws IOException {
+    token = config.getBearerToken();
+    if (token == null || token.trim().isEmpty()) {
+      throw new UnsupportedOperationException("Config option "
+          + BuiltInConnectionProperty.BEARER_TOKEN
+          + " must be specified to use ConstantBearerTokenProvider");
+    }
+  }
+
+  @Override
+  public synchronized String obtain(String username) {
+    return token;
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactoryTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactoryTest.java
new file mode 100644
index 000000000..601181a71
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactoryTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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.calcite.avatica.remote;
+
+import org.apache.calcite.avatica.BuiltInConnectionProperty;
+import org.apache.calcite.avatica.ConnectionConfig;
+import org.apache.calcite.avatica.ConnectionConfigImpl;
+import org.apache.calcite.avatica.ConnectionProperty;
+
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.BearerToken;
+import org.apache.hc.client5.http.auth.Credentials;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Properties;
+
+import static 
org.apache.calcite.avatica.remote.BearerTokenProviderFactoryTest.TestTokenProvider.*;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class BearerTokenProviderFactoryTest {
+  @Test
+  public void testConstantBearerToken() throws Exception {
+    Properties props = new Properties();
+    props.setProperty(BuiltInConnectionProperty.AUTHENTICATION.name(), 
"BEARER");
+    props.setProperty(BuiltInConnectionProperty.BEARER_TOKEN.name(), 
"testtoken");
+    ConnectionConfig config = new ConnectionConfigImpl(props);
+
+    BearerTokenProvider tokenProvider = 
BearerTokenProviderFactory.getBearerTokenProvider(config);
+    assertTrue("TokenProvider was not ConstantBearerTokenProvider",
+            tokenProvider instanceof ConstantBearerTokenProvider);
+    assertEquals("TokenProvider was not initialized",
+            "testtoken", tokenProvider.obtain("user"));
+  }
+
+  @Test
+  public void testCustomBearerToken() throws Exception {
+    Properties props = new Properties();
+    final TestConnectionProperty testProperty = new TestConnectionProperty();
+    props.setProperty(BuiltInConnectionProperty.TOKEN_PROVIDER_CLASS.name(),
+            TestTokenProvider.class.getName());
+    props.setProperty(testProperty.name(), "CustomToken");
+    ConnectionConfig config = new ConnectionConfigImpl(props);
+    BearerTokenProvider tokenProvider = 
BearerTokenProviderFactory.getBearerTokenProvider(config);
+    assertTrue("TokenProvider was not TestTokenProvider",
+            tokenProvider instanceof TestTokenProvider);
+    assertEquals("CustomToken", tokenProvider.obtain(USERNAME_1));
+    assertEquals(INVALID_TOKEN, tokenProvider.obtain(USERNAME_2));
+    assertNull(tokenProvider.obtain(USERNAME_3));
+    assertNull(tokenProvider.obtain(null));
+  }
+
+  @Test
+  public void testSetTokenProvider() throws Exception {
+    URL url = new URI("http://localhost:8765";).toURL();
+    Properties props = new Properties();
+    ConnectionConfig config = new ConnectionConfigImpl(props);
+
+    final TestConnectionProperty testProperty = new TestConnectionProperty();
+    props.setProperty(BuiltInConnectionProperty.TOKEN_PROVIDER_CLASS.name(),
+        TestTokenProvider.class.getName());
+    props.setProperty(testProperty.name(), "CustomToken");
+
+    AvaticaHttpClientFactory httpClientFactory = new 
AvaticaHttpClientFactoryImpl();
+    AvaticaHttpClient client = httpClientFactory.getClient(url, config, null);
+    assertTrue("Client was an instance of " + client.getClass(),
+        client instanceof AvaticaCommonsHttpClientImpl);
+
+    BearerTokenProvider tokenProvider = 
BearerTokenProviderFactory.getBearerTokenProvider(config);
+    assertTrue("TokenProvider was not TestTokenProvider",
+        tokenProvider instanceof TestTokenProvider);
+
+    // for user 1 tokenProvider returns a good token
+    ((AvaticaCommonsHttpClientImpl) client).setTokenProvider(USERNAME_1, 
tokenProvider);
+    Credentials res1 = ((AvaticaCommonsHttpClientImpl) 
client).context.getCredentialsProvider()
+        .getCredentials(new AuthScope(null, -1), 
((AvaticaCommonsHttpClientImpl) client).context);
+    assertEquals(((BearerToken) res1).getToken(), "CustomToken");
+
+    // for user 2 tokenProvider returns an invalid token
+    ((AvaticaCommonsHttpClientImpl) client).setTokenProvider(USERNAME_2, 
tokenProvider);
+    Credentials res2 = ((AvaticaCommonsHttpClientImpl) 
client).context.getCredentialsProvider()
+        .getCredentials(new AuthScope(null, -1), 
((AvaticaCommonsHttpClientImpl) client).context);
+    assertEquals(((BearerToken) res2).getToken(), INVALID_TOKEN);
+
+    // for user 3 tokenProvider returns null
+    ((AvaticaCommonsHttpClientImpl) client).setTokenProvider(USERNAME_3, 
tokenProvider);
+    Credentials res3 = ((AvaticaCommonsHttpClientImpl) 
client).context.getCredentialsProvider()
+        .getCredentials(new AuthScope(null, -1), 
((AvaticaCommonsHttpClientImpl) client).context);
+    assertTrue(res3 instanceof AvaticaCommonsHttpClientImpl.EmptyCredentials);
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testCustomBearerTokenInvalid() throws Exception {
+    Properties props = new Properties();
+    props.setProperty(
+            BuiltInConnectionProperty.TOKEN_PROVIDER_CLASS.name(),
+            TestTokenProvider.class.getName());
+    ConnectionConfig config = new ConnectionConfigImpl(props);
+    BearerTokenProviderFactory.getBearerTokenProvider(config);
+  }
+
+
+  @Test(expected = RuntimeException.class)
+  public void testInvalidBearerToken() throws Exception {
+    Properties props = new Properties();
+    props.setProperty(BuiltInConnectionProperty.HTTP_CLIENT_IMPL.name(),
+            Properties.class.getName()); // Properties is intentionally *not* 
a valid class
+    ConnectionConfig config = new ConnectionConfigImpl(props);
+    BearerTokenProviderFactory.getBearerTokenProvider(config);
+  }
+
+  public static class TestTokenProvider implements BearerTokenProvider {
+    public static final String USERNAME_1 = "USER1";
+    public static final String USERNAME_2 = "USER2";
+    public static final String USERNAME_3 = "USER3";
+    public static final String INVALID_TOKEN = "INV";
+
+    private final TestConnectionProperty testProperty = new 
TestConnectionProperty();
+    private String token;
+
+    @Override
+    public void init(ConnectionConfig config) throws IOException {
+      token = config.customPropertyValue(testProperty).getString();
+      if (token == null || token.trim().isEmpty()) {
+        throw new UnsupportedOperationException("Config option "
+                + testProperty.name()
+                + " must be specified to use ConstantBearerTokenProvider");
+      }
+    }
+
+    @Override
+    public synchronized String obtain(String username) {
+      try {
+        if (USERNAME_2.contentEquals(Objects.requireNonNull(username))) {
+          return INVALID_TOKEN;
+        } else if (USERNAME_1.contentEquals(Objects.requireNonNull(username))) 
{
+          return token;
+        } else {
+          return null;
+        }
+      } catch (NullPointerException exception) {
+        return null;
+      }
+    }
+
+    public static class TestConnectionProperty implements ConnectionProperty {
+      private final String name = "TEST_TOKEN_PROVIDER_PROPERTY";
+
+      public String name() {
+        return name.toUpperCase(Locale.ROOT);
+      }
+
+      public String camelName() {
+        return name.toLowerCase(Locale.ROOT);
+      }
+
+      public Object defaultValue() {
+        return null;
+      }
+
+      public Type type() {
+        return Type.STRING;
+      }
+
+      public Class valueClass() {
+        return Type.STRING.defaultValueClass();
+      }
+
+      public ConnectionConfigImpl.PropEnv wrap(Properties properties) {
+        final HashMap<String, ConnectionProperty> map = new HashMap<>();
+        map.put(name, this);
+        return new ConnectionConfigImpl.PropEnv(
+                ConnectionConfigImpl.parse(properties, map), this);
+      }
+
+      public boolean required() {
+        return false;
+      }
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProviderTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProviderTest.java
new file mode 100644
index 000000000..da60993d4
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProviderTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.calcite.avatica.remote;
+
+import org.apache.calcite.avatica.BuiltInConnectionProperty;
+import org.apache.calcite.avatica.ConnectionConfig;
+import org.apache.calcite.avatica.ConnectionConfigImpl;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+
+public class ConstantBearerTokenProviderTest {
+  static final String TOKEN = "test token";
+
+  ConnectionConfig conf;
+  @Before
+  public void setup() throws IOException {
+    Properties props = new Properties();
+    props.put(BuiltInConnectionProperty.BEARER_TOKEN.camelName(), TOKEN);
+    conf = new ConnectionConfigImpl(props);
+  }
+
+  @Test
+  public void testTokens() throws IOException {
+    ConstantBearerTokenProvider tokenProvider = new 
ConstantBearerTokenProvider();
+    tokenProvider.init(conf);
+    String token1 = tokenProvider.obtain("user1");
+    assertEquals(TOKEN, token1);
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testMissingConfig() throws IOException {
+    ConstantBearerTokenProvider tokenProvider = new 
ConstantBearerTokenProvider();
+    Properties props = new Properties();
+    ConnectionConfig emptyConf = new ConnectionConfigImpl(props);
+    tokenProvider.init(emptyConf);
+  }
+}
diff --git a/gradle.properties b/gradle.properties
index 3034e0663..8676bf978 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -80,3 +80,4 @@ protobuf.version=3.25.8
 scott-data-hsqldb.version=0.1
 servlet.version=4.0.1
 slf4j.version=1.7.25
+commons-io.version=2.18.0


Reply via email to