diff --git a/README b/README index 7a4eaaeafe..571202929f 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -Apache Calcite Avatica release 1.23.0 +Apache Calcite Avatica release 1.24.0 # Overview This is a source or binary distribution of Avatica, a framework for diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index 294af95608..04ae013471 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -78,6 +78,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 34b64ce668..441a87dead 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -42,6 +42,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 d68d39dbf5..ab76ad534e 100644 --- a/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java +++ b/core/src/main/java/org/apache/calcite/avatica/BuiltInConnectionProperty.java @@ -127,7 +127,13 @@ public enum BuiltInConnectionProperty implements ConnectionProperty { * HTTP Connection Timeout in milliseconds. */ HTTP_CONNECTION_TIMEOUT("http_connection_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("bearertoken", Type.STRING, null, 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 adb98339b8..ca10fa27e1 100644 --- a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java +++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfig.java @@ -81,6 +81,12 @@ public interface ConnectionConfig { long getLBConnectionFailoverSleepTime(); /** @see BuiltInConnectionProperty#HTTP_CONNECTION_TIMEOUT **/ long getHttpConnectionTimeout(); + /** @see BuiltInConnectionProperty#BEARER_TOKEN */ + String bearerToken(); + /** @see BuiltInConnectionProperty#TOKEN_PROVIDER_CLASS */ + String bearerTokenProviderClass(); + + 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 9ae4446e79..196fd24b73 100644 --- a/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java +++ b/core/src/main/java/org/apache/calcite/avatica/ConnectionConfigImpl.java @@ -168,6 +168,17 @@ public long getLBConnectionFailoverSleepTime() { public long getHttpConnectionTimeout() { return BuiltInConnectionProperty.HTTP_CONNECTION_TIMEOUT.wrap(properties).getLong(); } + public String bearerToken() { + return BuiltInConnectionProperty.BEARER_TOKEN.wrap(properties).getString(); + } + + public String bearerTokenProviderClass() { + 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 @@ -198,7 +209,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 1c5327f13a..9e2b469d7a 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 @@ -60,6 +60,9 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -68,7 +71,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 @@ -91,6 +94,10 @@ public class AvaticaCommonsHttpClientImpl implements AvaticaHttpClient, HttpClie protected CredentialsProvider credentialsProvider = null; protected Lookup authRegistry = null; protected Object userToken; + private static final List AVATICA_SCHEME_PRIORITY = + Collections.unmodifiableList(Arrays.asList(StandardAuthScheme.BASIC, + StandardAuthScheme.DIGEST, StandardAuthScheme.SPNEGO, StandardAuthScheme.NTLM, + StandardAuthScheme.KERBEROS, "Bearer")); public AvaticaCommonsHttpClientImpl(URL url) { this.uri = toURI(Objects.requireNonNull(url)); @@ -104,6 +111,7 @@ protected void initializeClient(PoolingHttpClientConnectionManager pool, RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); RequestConfig requestConfig = requestConfigBuilder .setConnectTimeout(config.getHttpConnectionTimeout(), TimeUnit.MILLISECONDS) + .setTargetPreferredAuthSchemes(AVATICA_SCHEME_PRIORITY) .build(); HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(pool) .setDefaultRequestConfig(requestConfig); @@ -206,6 +214,17 @@ CloseableHttpResponse execute(HttpPost post, HttpClientContext context) } } + @Override public void setTokenProvider(String username, BearerTokenProvider tokenProvider) { + this.credentialsProvider = new BasicCredentialsProvider(); + ((BasicCredentialsProvider) this.credentialsProvider) + .setCredentials(anyAuthScope, new BearerCredentials(username, tokenProvider)); + + this.authRegistry = RegistryBuilder.create() + .register("Bearer", + new BearerSchemeFactory()) + .build(); + } + /** * A credentials implementation which returns null. */ 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 0b9d66c07c..621043a461 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 @@ -130,6 +130,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..5e0094eecd --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerAuthenticateable.java @@ -0,0 +1,22 @@ +/* + * 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; + +public interface BearerAuthenticateable { + + void setTokenProvider(String username, BearerTokenProvider tokenProvider); +} diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/BearerCredentials.java b/core/src/main/java/org/apache/calcite/avatica/remote/BearerCredentials.java new file mode 100644 index 0000000000..693be548d7 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerCredentials.java @@ -0,0 +1,54 @@ +/* + * 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.hc.client5.http.auth.Credentials; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.util.Args; + +import java.io.Serializable; +import java.security.Principal; + +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class BearerCredentials implements Credentials, Serializable { + + private final BearerTokenProvider tokenProvider; + + private final String userName; + + public BearerCredentials(final String userName, final BearerTokenProvider tokenProvider) { + Args.notNull(userName, "userName"); + Args.notNull(tokenProvider, "tokenProvider"); + this.tokenProvider = tokenProvider; + this.userName = userName; + } + + public String getToken() { + return tokenProvider.obtain(userName); + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public char[] getPassword() { + return null; + } +} diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/BearerScheme.java b/core/src/main/java/org/apache/calcite/avatica/remote/BearerScheme.java new file mode 100644 index 0000000000..35e6aa94df --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerScheme.java @@ -0,0 +1,142 @@ +/* + * 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.hc.client5.http.auth.AuthChallenge; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.AuthenticationException; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class BearerScheme implements AuthScheme, Serializable { + private static final Logger LOG = LoggerFactory.getLogger(BearerScheme.class); + private String token; + + private final Map paramMap; + private boolean complete; + + public BearerScheme() { + super(); + this.paramMap = new HashMap<>(); + this.complete = false; + } + + @Override + public String getName() { + return "Bearer"; + } + + @Override + public boolean isConnectionBased() { + return false; + } + + @Override + public String getRealm() { + return this.paramMap.get("realm"); + } + + @Override + public void processChallenge( + final AuthChallenge authChallenge, + final HttpContext context) throws MalformedChallengeException { + this.paramMap.clear(); + final List params = authChallenge.getParams(); + if (params != null) { + for (final NameValuePair param: params) { + this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue()); + } + if (LOG.isDebugEnabled()) { + final String error = paramMap.get("error"); + if (error != null) { + final StringBuilder buf = new StringBuilder(); + buf.append(error); + final String desc = paramMap.get("error_description"); + final String uri = paramMap.get("error_uri"); + if (desc != null || uri != null) { + buf.append(" ("); + buf.append(desc).append("; ").append(uri); + buf.append(")"); + } + LOG.debug(buf.toString()); + } + } + } + this.complete = true; + } + + @Override + public boolean isChallengeComplete() { + return this.complete; + } + + @Override + public boolean isResponseReady( + final HttpHost host, + final CredentialsProvider credentialsProvider, + final HttpContext context) throws AuthenticationException { + Args.notNull(host, "Auth host"); + Args.notNull(credentialsProvider, "CredentialsProvider"); + + final Credentials credentials = credentialsProvider.getCredentials( + new AuthScope(host, null, getName()), context); + + if (!(credentials instanceof BearerCredentials)) { + return false; + } + + this.token = ((BearerCredentials) credentials).getToken(); + return null != this.token; + } + + @Override + public Principal getPrincipal() { + return null; + } + + @Override + public String generateAuthResponse( + final HttpHost host, + final HttpRequest request, + final HttpContext context) throws AuthenticationException { + Asserts.notNull(this.token, "Bearer token"); + return "Bearer " + this.token; + } + + @Override + public String toString() { + return getName() + this.paramMap; + } +} diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/BearerSchemeFactory.java b/core/src/main/java/org/apache/calcite/avatica/remote/BearerSchemeFactory.java new file mode 100644 index 0000000000..ec634faa0e --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerSchemeFactory.java @@ -0,0 +1,34 @@ +/* + * 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.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.protocol.HttpContext; + +@Contract(threading = ThreadingBehavior.STATELESS) +public class BearerSchemeFactory implements AuthSchemeFactory { + public static final BearerSchemeFactory INSTANCE = new BearerSchemeFactory(); + + @Override + public AuthScheme create(final HttpContext context) { + return new BearerScheme(); + } + +} 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..ea291b2ee1 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/remote/BearerTokenProvider.java @@ -0,0 +1,28 @@ +/* + * 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; + +public interface BearerTokenProvider { + + void init(ConnectionConfig config) throws IOException; + + 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..deef1fb69d --- /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.bearerTokenProviderClass(); + 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..64fdea4280 --- /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.bearerToken(); + 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/main/java/org/apache/calcite/avatica/util/Unsafe.java b/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java index 906651d92e..32449b1a2f 100644 --- a/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java +++ b/core/src/main/java/org/apache/calcite/avatica/util/Unsafe.java @@ -45,6 +45,11 @@ public static void wait(Object o) throws InterruptedException { o.wait(); } + /** Calls {@link Object#wait(long timeout)}. */ + public static void wait(Object o, long timeout) throws InterruptedException { + o.wait(timeout); + } + /** Returns a {@link java.util.Calendar} with the local time zone and root * locale. */ public static Calendar localCalendar() { diff --git a/core/src/test/java/org/apache/calcite/avatica/remote/BearerSchemeTest.java b/core/src/test/java/org/apache/calcite/avatica/remote/BearerSchemeTest.java new file mode 100644 index 0000000000..659ec807df --- /dev/null +++ b/core/src/test/java/org/apache/calcite/avatica/remote/BearerSchemeTest.java @@ -0,0 +1,136 @@ +/* + * 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.hc.client5.http.auth.AuthChallenge; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.message.BasicNameValuePair; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.*; +import java.util.Properties; + +import static org.junit.Assert.*; + + +/** + * Bearer authentication test cases. + * This file is based on Apache HttpComponents Client project + * https://github.com/apache/httpcomponents-client/blob/master/ + * httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java + */ +public class BearerSchemeTest { + ConnectionConfig conf; + @Before + public void setup() throws IOException { + Properties props = new Properties(); + props.put(BuiltInConnectionProperty.AVATICA_USER.camelName(), "testUser"); + props.put(BuiltInConnectionProperty.BEARER_TOKEN.camelName(), "token1"); + conf = new ConnectionConfigImpl(props); + } + + @Test + public void testBearerAuthenticationEmptyChallenge() throws Exception { + final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "BEARER"); + final AuthScheme authscheme = new BearerScheme(); + authscheme.processChallenge(authChallenge, null); + assertNull(authscheme.getRealm()); + } + + @Test + public void testBearerAuthentication() throws Exception { + final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "Bearer", + new BasicNameValuePair("realm", "test")); + + final AuthScheme authscheme = new BearerScheme(); + authscheme.processChallenge(authChallenge, null); + + final HttpHost host = new HttpHost("somehost", 80); + final ConstantBearerTokenProvider tokenProvider = new ConstantBearerTokenProvider(); + tokenProvider.init(conf); + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(host, "test", null), + new BearerCredentials("testUser", tokenProvider)) + .build(); + + final HttpRequest request = new BasicHttpRequest("GET", "/"); + assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + assertEquals("Bearer token1", authscheme.generateAuthResponse(host, request, null)); + + assertEquals("test", authscheme.getRealm()); + assertTrue(authscheme.isChallengeComplete()); + assertFalse(authscheme.isConnectionBased()); + } + + @Ignore //TODO + @Test + public void testNoTokenForUser() throws Exception { + final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "Bearer", + new BasicNameValuePair("realm", "test")); + + final AuthScheme authscheme = new BearerScheme(); + authscheme.processChallenge(authChallenge, null); + + final HttpHost host = new HttpHost("somehost", 80); + final ConstantBearerTokenProvider tokenProvider = new ConstantBearerTokenProvider(); + tokenProvider.init(conf); + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(host, "test", null), + new BearerCredentials("testUser2", tokenProvider)) + .build(); + + final HttpRequest request = new BasicHttpRequest("GET", "/"); + assertFalse(authscheme.isResponseReady(host, credentialsProvider, null)); + } + + @Test + public void testSerialization() throws Exception { + final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "Bearer", + new BasicNameValuePair("realm", "test"), + new BasicNameValuePair("code", "read")); + + final AuthScheme authscheme = new BearerScheme(); + authscheme.processChallenge(authChallenge, null); + + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + final ObjectOutputStream out = new ObjectOutputStream(buffer); + out.writeObject(authscheme); + out.flush(); + final byte[] raw = buffer.toByteArray(); + final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(raw)); + final BearerScheme authscheme2 = (BearerScheme) in.readObject(); + + assertEquals(authscheme2.getName(), authscheme2.getName()); + assertEquals(authscheme2.getRealm(), authscheme2.getRealm()); + assertEquals(authscheme.isChallengeComplete(), authscheme2.isChallengeComplete()); + } + +} 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..7b7c09d5fa --- /dev/null +++ b/core/src/test/java/org/apache/calcite/avatica/remote/BearerTokenProviderFactoryTest.java @@ -0,0 +1,149 @@ +/* + * 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.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Properties; + +import static org.apache.calcite.avatica.remote.BearerTokenProviderFactoryTest.TestTokenProvider.*; + +import static org.junit.Assert.assertEquals; +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("TokenProvider was not initialized", + "CustomToken", tokenProvider.obtain(USERNAME_1)); + assertEquals("TokenProvider was not initialized", + INVALID_TOKEN, tokenProvider.obtain(USERNAME_2)); + } + + @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 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) { + if (USERNAME_2.contentEquals(username)) { + return INVALID_TOKEN; + } + return token; + } + + 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 6b59c1c6e3..eaef1980ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -76,3 +76,4 @@ protobuf.version=3.21.9 scott-data-hsqldb.version=0.1 servlet.version=4.0.1 slf4j.version=1.7.25 +commons-io.version=2.15.0 diff --git a/site/_docs/docker_images.md b/site/_docs/docker_images.md index 2354ca3cbf..e04a253109 100644 --- a/site/_docs/docker_images.md +++ b/site/_docs/docker_images.md @@ -70,22 +70,22 @@ file will start an instance of PostgreSQL and an instance of the Avatica server, exposing an Avatica server configured against a "real" PostgreSQL database. All of the `Dockerfile` and `docker-compose.yml` files are conveniently provided in an archive for -each release. Here is the layout for release 1.23.0: +each release. Here is the layout for release 1.24.0: ``` -avatica-docker-1.23.0/ -avatica-docker-1.23.0/hypersql/ -avatica-docker-1.23.0/mysql/ -avatica-docker-1.23.0/postgresql/ -avatica-docker-1.23.0/Dockerfile -avatica-docker-1.23.0/hypersql/build.sh -avatica-docker-1.23.0/hypersql/Dockerfile -avatica-docker-1.23.0/mysql/build.sh -avatica-docker-1.23.0/mysql/docker-compose.yml -avatica-docker-1.23.0/mysql/Dockerfile -avatica-docker-1.23.0/postgresql/build.sh -avatica-docker-1.23.0/postgresql/docker-compose.yml -avatica-docker-1.23.0/postgresql/Dockerfile +avatica-docker-1.24.0/ +avatica-docker-1.24.0/hypersql/ +avatica-docker-1.24.0/mysql/ +avatica-docker-1.24.0/postgresql/ +avatica-docker-1.24.0/Dockerfile +avatica-docker-1.24.0/hypersql/build.sh +avatica-docker-1.24.0/hypersql/Dockerfile +avatica-docker-1.24.0/mysql/build.sh +avatica-docker-1.24.0/mysql/docker-compose.yml +avatica-docker-1.24.0/mysql/Dockerfile +avatica-docker-1.24.0/postgresql/build.sh +avatica-docker-1.24.0/postgresql/docker-compose.yml +avatica-docker-1.24.0/postgresql/Dockerfile ``` #### Running diff --git a/site/_docs/history.md b/site/_docs/history.md index 69693aa35a..43e31797fa 100644 --- a/site/_docs/history.md +++ b/site/_docs/history.md @@ -28,6 +28,73 @@ For a full list of releases, see Downloads are available on the [downloads page]({{ site.baseurl }}/downloads/avatica.html). +## 1.24.0 / 2023-12-XX +{: #v1-24-0} + +Apache Calcite Avatica 1.24.0 features mostly dependency upgrades with some minor bug fixes and features. + +Compatibility: This release is tested +on Linux, macOS, Microsoft Windows; +using Oracle JDK 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19; +using IBM Java 8; +Guava versions 14.0.1 to 32.1.1-jre; +other software versions as specified in `gradle.properties`. + +Contributors to this release: +Evgeniy Stanilovskiy, +Francis Chuang (Release Manager), +Greg Hart, +Istvan Toth, +Mihai Budiu, +Richard Antal, +Sergey Nuyanzin, +TJ Banghart +Vaibhav Joshi, +Will Noble + +Features and bug fixes + +* [CALCITE-5494] + Time zone tests in DateTimeUtilsTest should pass in Europe/London +* [CALCITE-5440] + Bump gradle from 7.4.2 to 7.6.1 +* Bump forbidden apis from 3.2 to 3.4 +* [CALCITE-5567] + Update mockito from 4.4.0 to 4.11.0 and enable jdk19 +* [CALCITE-5678] + Validate date, time and timestamp literals per ISO-8601 +* [CALCITE-5581] + Implement Basic client side load balancing in Avatica Driver +* [CALCITE-5803] + Migrate Avatica to Gradle 8.1.1 +* [CALCITE-5812] + Gradle tasks fails when creating the javadoc aggregation + Exclude "bom" project from the javadoc aggregation since it does not have "main" and "test" objects causing "tasks" to + fail. +* [CALCITE-5804] + Upgrade jackson version from 2.14.1 to 2.15.2 +* [CALCITE-5748] + Support Guava 32.1.1-jre +* [CALCITE-5890] + Handle non-JKS truststores in Avatica client +* [CALCITE-5981] + `TIMESTAMPDIFF` function returns incorrect result +* [CALCITE-6034] + Add `isAutoIncrement` and `isGenerated` args to `MetaColumn` constructor +* [CALCITE-5536] + Clean up some of the magic numbers in `AvaticaResultSetConversionsTest` and `AbstractCursor` +* [CALCITE-6113] + Update HttpComponents Core to 5.2.3 and HttpComponents Client to 5.2.1 in Avatica + +Build and tests + +* Install svn in docker release script +* [CALCITE-6106] + Switch from gradle to eclipse-temurin image for avatica docker-compose release commands +* [CALCITE-6107] + Upgrade vlsi-release-plugins to 1.90 +* Use eclipse-temurin:8 images + ## 1.23.0 / 2023-01-19 {: #v1-23-0} diff --git a/site/_docs/howto.md b/site/_docs/howto.md index e36e20c39c..04ab88b710 100644 --- a/site/_docs/howto.md +++ b/site/_docs/howto.md @@ -31,7 +31,7 @@ Here's some miscellaneous documentation about using Avatica. ## Building from a source distribution Prerequisites are Java (JDK 8 or later) -and Gradle (version 7.4.2) on your path. +and Gradle (version 8.1.1) on your path. (The source distribution [does not include the Gradle wrapper](https://issues.apache.org/jira/browse/CALCITE-4575); @@ -43,8 +43,8 @@ Unpack the source distribution `.tar.gz` file, then build using Gradle: {% highlight bash %} -$ tar xvfz apache-calcite-avatica-1.23.0-src.tar.gz -$ cd apache-calcite-avatica-1.23.0-src +$ tar xvfz apache-calcite-avatica-1.24.0-src.tar.gz +$ cd apache-calcite-avatica-1.24.0-src $ gradle build {% endhighlight %}