diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java b/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java index 123f82170..c23ce1638 100644 --- a/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java +++ b/core/src/main/java/org/apache/calcite/avatica/remote/DoAsAvaticaHttpClient.java @@ -16,9 +16,10 @@ */ package org.apache.calcite.avatica.remote; -import java.security.PrivilegedAction; +import org.apache.calcite.avatica.util.SecurityUtils; + import java.util.Objects; -import javax.security.auth.Subject; +import java.util.concurrent.Callable; /** * HTTP client implementation which invokes the wrapped HTTP client in a doAs with the provided @@ -33,9 +34,10 @@ public DoAsAvaticaHttpClient(AvaticaHttpClient wrapped, KerberosConnection kerbe this.kerberosUtil = Objects.requireNonNull(kerberosUtil); } - @Override public byte[] send(final byte[] request) { - return Subject.doAs(kerberosUtil.getSubject(), new PrivilegedAction() { - @Override public byte[] run() { + @Override + public byte[] send(final byte[] request) { + return SecurityUtils.callAs(kerberosUtil.getSubject(), new Callable() { + @Override public byte[] call() { return wrapped.send(request); } }); diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java b/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java index 438d53336..5a752385f 100644 --- a/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java +++ b/core/src/main/java/org/apache/calcite/avatica/remote/KerberosConnection.java @@ -172,7 +172,7 @@ Entry createRenewalThread(LoginContext originalContext, renewalPeriod); Thread t = new Thread(task); - // Don't prevent the JVM from existing + // Don't prevent the JVM from exiting t.setDaemon(true); // Log an error message if this thread somehow dies t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { diff --git a/core/src/main/java/org/apache/calcite/avatica/util/SecurityUtils.java b/core/src/main/java/org/apache/calcite/avatica/util/SecurityUtils.java new file mode 100644 index 000000000..b6548d96a --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/util/SecurityUtils.java @@ -0,0 +1,203 @@ +/* + * 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.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import javax.security.auth.Subject; + +/** + * This class is loosely based on SecurityUtils in Jetty 12.0 + * + *

Collections of utility methods to deal with the scheduled removal + * of the security classes defined by JEP 411.

+ */ +public class SecurityUtils { + private static final MethodHandle CALL_AS = lookupCallAs(); + private static final MethodHandle CURRENT = lookupCurrent(); + private static final MethodHandle DO_PRIVILEGED = lookupDoPrivileged(); + + private SecurityUtils() { + } + + private static MethodHandle lookupCallAs() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + try { + // Subject.doAs() is deprecated for removal and replaced by Subject.callAs(). + // Lookup first the new API, since for Java versions where both exist, the + // new API delegates to the old API (for example Java 18, 19 and 20). + // Otherwise (Java 17), lookup the old API. + return lookup.findStatic(Subject.class, "callAs", + MethodType.methodType(Object.class, Subject.class, Callable.class)); + } catch (NoSuchMethodException x) { + try { + // Lookup the old API. + MethodType oldSignature = + MethodType.methodType(Object.class, Subject.class, PrivilegedExceptionAction.class); + MethodHandle doAs = lookup.findStatic(Subject.class, "doAs", oldSignature); + // Convert the Callable used in the new API to the PrivilegedAction used in the old + // API. + MethodType convertSignature = + MethodType.methodType(PrivilegedExceptionAction.class, Callable.class); + MethodHandle converter = + lookup.findStatic(SecurityUtils.class, "callableToPrivilegedExceptionAction", + convertSignature); + return MethodHandles.filterArguments(doAs, 1, converter); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupDoPrivileged() { + try { + // Use reflection to work with Java versions that have and don't have AccessController. + Class klass = + ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController"); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + return lookup.findStatic(klass, "doPrivileged", + MethodType.methodType(Object.class, PrivilegedAction.class)); + } catch (NoSuchMethodException | IllegalAccessException x) { + // Assume that single methods won't be removed from AcessController + throw new AssertionError(x); + } catch (ClassNotFoundException e) { + return null; + } + } + + private static MethodHandle lookupCurrent() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + // Subject.getSubject(AccessControlContext) is deprecated for removal and replaced by + // Subject.current(). + // Lookup first the new API, since for Java versions where both exists, the + // new API delegates to the old API (for example Java 18, 19 and 20). + // Otherwise (Java 17), lookup the old API. + return lookup.findStatic(Subject.class, "current", + MethodType.methodType(Subject.class)); + } catch (NoSuchMethodException e) { + MethodHandle getContext = lookupGetContext(); + MethodHandle getSubject = lookupGetSubject(); + return MethodHandles.filterReturnValue(getContext, getSubject); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupGetSubject() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + Class contextklass = + ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessControlContext"); + return lookup.findStatic(Subject.class, "getSubject", + MethodType.methodType(Subject.class, contextklass)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupGetContext() { + try { + // Use reflection to work with Java versions that have and don't have AccessController. + Class controllerKlass = + ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController"); + Class contextklass = + ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessControlContext"); + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + return lookup.findStatic(controllerKlass, "getContext", + MethodType.methodType(contextklass)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + /** + * Maps to AccessController#doPrivileged if available, otherwise returns action.run(). + * @param action the action to run + * @return the result of running the action + * @param the type of the result + */ + public static T doPrivileged(PrivilegedAction action) { + // Keep this method short and inlineable. + if (DO_PRIVILEGED == null) { + return action.run(); + } + return doPrivileged(DO_PRIVILEGED, action); + } + + private static T doPrivileged(MethodHandle doPrivileged, PrivilegedAction action) { + try { + return (T) doPrivileged.invoke(action); + } catch (Throwable t) { + throw sneakyThrow(t); + } + } + + /** + * Maps to Subject.callAs() if available, otherwise maps to Subject.doAs() + * @param subject the subject this action runs as + * @param action the action to run + * @return the result of the action + * @param the type of the result + * @throws CompletionException + */ + public static T callAs(Subject subject, Callable action) throws CompletionException { + try { + return (T) CALL_AS.invoke(subject, action); + } catch (PrivilegedActionException e) { + throw new CompletionException(e.getCause()); + } catch (Throwable t) { + throw sneakyThrow(t); + } + } + + /** + * Maps to Subject.currect() is available, otherwise maps to Subject.getSubject() + * @return the current subject + */ + public static Subject currentSubject() { + try { + return (Subject) CURRENT.invoke(); + } catch (Throwable t) { + throw sneakyThrow(t); + } + } + + @SuppressWarnings("unused") + private static PrivilegedExceptionAction callableToPrivilegedExceptionAction( + Callable callable) { + return callable::call; + } + + @SuppressWarnings("unchecked") + private static RuntimeException sneakyThrow(Throwable e) throws E { + throw (E) e; + } +} diff --git a/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java b/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java index ce9c2c989..9c6b44cb2 100644 --- a/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java +++ b/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java @@ -21,6 +21,7 @@ import org.apache.calcite.avatica.remote.Driver.Serialization; import org.apache.calcite.avatica.remote.Service; import org.apache.calcite.avatica.remote.Service.RpcMetadataResponse; +import org.apache.calcite.avatica.util.SecurityUtils; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.ConfigurableSpnegoLoginService; @@ -53,7 +54,6 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.security.Principal; -import java.security.PrivilegedAction; import java.security.SecureRandom; import java.time.Duration; import java.util.ArrayList; @@ -205,8 +205,8 @@ static AvaticaHandler wrapJettyHandler(Handler handler) { public void start() { if (null != subject) { // Run the start in the privileged block (as the kerberos-identified user) - Subject.doAs(subject, new PrivilegedAction() { - @Override public Void run() { + SecurityUtils.callAs(subject, new Callable() { + @Override public Void call() { internalStart(); return null; } diff --git a/server/src/main/java/org/apache/calcite/avatica/server/SubjectPreservingPrivilegedThreadFactory.java b/server/src/main/java/org/apache/calcite/avatica/server/SubjectPreservingPrivilegedThreadFactory.java index 7a6d6b684..b0df42a2b 100644 --- a/server/src/main/java/org/apache/calcite/avatica/server/SubjectPreservingPrivilegedThreadFactory.java +++ b/server/src/main/java/org/apache/calcite/avatica/server/SubjectPreservingPrivilegedThreadFactory.java @@ -16,8 +16,10 @@ */ package org.apache.calcite.avatica.server; -import java.security.AccessController; +import org.apache.calcite.avatica.util.SecurityUtils; + import java.security.PrivilegedAction; +import java.util.concurrent.Callable; import java.util.concurrent.ThreadFactory; import javax.security.auth.Subject; @@ -39,12 +41,13 @@ class SubjectPreservingPrivilegedThreadFactory implements ThreadFactory { * @param Runnable object for the thread * @return a new thread, protected from classloader pinning, but keeping the current Subject */ + @Override public Thread newThread(Runnable runnable) { - Subject subject = Subject.getSubject(AccessController.getContext()); - return AccessController.doPrivileged(new PrivilegedAction() { + Subject subject = SecurityUtils.currentSubject(); + return SecurityUtils.doPrivileged(new PrivilegedAction() { @Override public Thread run() { - return Subject.doAs(subject, new PrivilegedAction() { - @Override public Thread run() { + return SecurityUtils.callAs(subject, new Callable() { + @Override public Thread call() { Thread thread = new Thread(runnable); thread.setDaemon(true); thread.setName("avatica_qtp" + hashCode() + "-" + thread.getId()); diff --git a/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java b/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java index aeb63a16c..1c831ee5b 100644 --- a/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java +++ b/server/src/test/java/org/apache/calcite/avatica/AvaticaSpnegoTest.java @@ -19,6 +19,7 @@ import org.apache.calcite.avatica.remote.Driver; import org.apache.calcite.avatica.server.AvaticaJaasKrbUtil; import org.apache.calcite.avatica.server.HttpServer; +import org.apache.calcite.avatica.util.SecurityUtils; import org.apache.kerby.kerberos.kerb.KrbException; import org.apache.kerby.kerberos.kerb.client.KrbConfig; @@ -34,13 +35,13 @@ import org.slf4j.LoggerFactory; import java.io.File; -import java.security.PrivilegedExceptionAction; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; import javax.security.auth.Subject; import static org.junit.Assert.assertEquals; @@ -217,8 +218,8 @@ public AvaticaSpnegoTest(String jdbcUrl) { // The name of the principal // Run this code, logged in as the subject (the client) - Subject.doAs(clientSubject, new PrivilegedExceptionAction() { - @Override public Void run() throws Exception { + SecurityUtils.callAs(clientSubject, new Callable() { + @Override public Void call() throws Exception { try (Connection conn = DriverManager.getConnection(jdbcUrl)) { try (Statement stmt = conn.createStatement()) { assertFalse(stmt.execute("DROP TABLE IF EXISTS " + tableName)); diff --git a/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java b/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java index 03857d8e4..11e435530 100644 --- a/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java +++ b/server/src/test/java/org/apache/calcite/avatica/SpnegoTestUtil.java @@ -19,6 +19,7 @@ import org.apache.calcite.avatica.remote.KerberosConnection; import org.apache.calcite.avatica.remote.Service.RpcMetadataResponse; import org.apache.calcite.avatica.server.AvaticaHandler; +import org.apache.calcite.avatica.util.SecurityUtils; import org.apache.kerby.kerberos.kerb.KrbException; import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; @@ -38,7 +39,6 @@ import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.nio.charset.StandardCharsets; -import java.security.AccessController; import java.security.Principal; import java.security.PrivilegedAction; import javax.security.auth.login.Configuration; @@ -138,8 +138,9 @@ public static void refreshJaasConfiguration() { // Configuration keeps a static instance of Configuration that it will return once it // has been initialized. We need to nuke that static instance to make sure our // serverSpnegoConfigFile gets read. - AccessController.doPrivileged(new PrivilegedAction() { - public Configuration run() { + SecurityUtils.doPrivileged(new PrivilegedAction() { + @Override + public Configuration run() { return Configuration.getConfiguration(); } }).refresh(); diff --git a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java b/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java index ad09b6f7d..8481971e2 100644 --- a/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java +++ b/server/src/test/java/org/apache/calcite/avatica/server/HttpServerSpnegoWithoutJaasTest.java @@ -22,6 +22,7 @@ import org.apache.calcite.avatica.SpnegoTestUtil; import org.apache.calcite.avatica.remote.AvaticaCommonsHttpClientImpl; import org.apache.calcite.avatica.remote.CommonsHttpClientPoolCache; +import org.apache.calcite.avatica.util.SecurityUtils; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.kerby.kerberos.kerb.KrbException; @@ -47,9 +48,9 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.Principal; -import java.security.PrivilegedExceptionAction; import java.util.Properties; import java.util.Set; +import java.util.concurrent.Callable; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosTicket; @@ -216,8 +217,8 @@ private static void setupUsers(File keytabDir) throws KrbException { final String principalName = clientPrincipals.iterator().next().getName(); // Run this code, logged in as the subject (the client) - byte[] response = Subject.doAs(clientSubject, new PrivilegedExceptionAction() { - @Override public byte[] run() throws Exception { + byte[] response = SecurityUtils.callAs(clientSubject, new Callable() { + @Override public byte[] call() throws Exception { // Logs in with Kerberos via GSS GSSManager gssManager = GSSManager.getInstance(); Oid oid = new Oid(SpnegoTestUtil.JGSS_KERBEROS_TICKET_OID);