From 414f6d96497c48c211d0012448e562cf54134ed0 Mon Sep 17 00:00:00 2001 From: Richard Antal Date: Mon, 9 Feb 2026 13:19:41 +0100 Subject: [PATCH] [CALCITE-6135] BEARER authentication support Co-authored-by: Aron Meszaros --- bom/build.gradle.kts | 1 + core/build.gradle.kts | 1 + .../avatica/BuiltInConnectionProperty.java | 13 +- .../calcite/avatica/ConnectionConfig.java | 8 + .../calcite/avatica/ConnectionConfigImpl.java | 18 +- .../calcite/avatica/ConnectionProperty.java | 2 +- .../avatica/ConnectionPropertyValue.java | 113 ++++++++++ .../avatica/remote/AuthenticationType.java | 1 + .../remote/AvaticaCommonsHttpClientImpl.java | 35 ++- .../remote/AvaticaHttpClientFactoryImpl.java | 18 ++ .../remote/BearerAuthenticateable.java | 31 +++ .../avatica/remote/BearerTokenProvider.java | 41 ++++ .../remote/BearerTokenProviderFactory.java | 62 ++++++ .../remote/ConstantBearerTokenProvider.java | 41 ++++ .../BearerTokenProviderFactoryTest.java | 204 ++++++++++++++++++ .../ConstantBearerTokenProviderTest.java | 57 +++++ gradle.properties | 1 + 17 files changed, 642 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/org/apache/calcite/avatica/ConnectionPropertyValue.java create mode 100644 core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java create mode 100644 core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java create mode 100644 core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactory.java create mode 100644 core/src/main/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProvider.java create mode 100644 core/src/test/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactoryTest.java create mode 100644 core/src/test/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProviderTest.java diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index 6f46ab9a62..1eb8a98ee1 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 12bdb490a2..ffa8ecc6d7 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 56112045e1..00034cbd5f 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 69b98f54d2..bdf36041d2 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 6326de55df..fd4728acb7 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 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 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 b41b9a31a8..f0245bfa24 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 0000000000..888b00323c --- /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 getEnum(Class enumClass); + + /** + * Returns the enum value of this property. Throws if not set and no + * default. + */ + > E getEnum(Class enumClass, E defaultValue); + + /** + * Returns an instance of a plugin. + * + *

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 getPlugin(Class pluginClass, T defaultInstance); + + /** + * Returns an instance of a plugin, using a given class name if none is + * set. + * + *

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 getPlugin(Class 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 f483be9bdf..8bdb7709a2 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 18a7e32cfe..7c092bcdb8 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.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 e8fcb77dc9..445848979d 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/BearerAuthenticateable.java b/core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java new file mode 100644 index 0000000000..8d7f535768 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * Interface that allows configuration of a username and BearerTokenProvider HTTP authentication. + */ +public interface BearerAuthenticateable { + + /** + * 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/BearerTokenProvider.java b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java new file mode 100644 index 0000000000..994f18ac0c --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * Interface that provides bearer token for authentication. + */ +public interface BearerTokenProvider { + + /** + * 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 0000000000..f6dda63b72 --- /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 clz = + Class.forName(className).asSubclass(BearerTokenProvider.class); + Constructor 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/ConstantBearerTokenProvider.java b/core/src/main/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProvider.java new file mode 100644 index 0000000000..4a350c1920 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/ConstantBearerTokenProvider.java @@ -0,0 +1,41 @@ +/* + * 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 java.io.IOException; + +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 0000000000..601181a711 --- /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 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 0000000000..da60993d44 --- /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 3034e0663b..8676bf9787 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