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