From b254987a331ca9d2a08e80c2848c000d75c5b90e Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Tue, 24 Sep 2024 13:21:39 +0200 Subject: [PATCH] [CALCITE-6590] Use reflection to handle Java SecurityManager deprecation in Avatica --- .../avatica/remote/DoAsAvaticaHttpClient.java | 12 +- .../avatica/remote/KerberosConnection.java | 2 +- .../calcite/avatica/util/SecurityUtils.java | 268 ++++++++++++++++++ .../calcite/avatica/server/HttpServer.java | 6 +- ...jectPreservingPrivilegedThreadFactory.java | 17 +- .../calcite/avatica/AvaticaSpnegoTest.java | 7 +- .../calcite/avatica/SpnegoTestUtil.java | 7 +- .../HttpServerSpnegoWithoutJaasTest.java | 7 +- 8 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/org/apache/calcite/avatica/util/SecurityUtils.java 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 123f821703..c23ce1638d 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 438d533367..5a752385fa 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 0000000000..f089587e5b --- /dev/null +++ b/core/src/main/java/org/apache/calcite/avatica/util/SecurityUtils.java @@ -0,0 +1,268 @@ +/* + * 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.Permission; +import java.security.PrivilegedAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import javax.security.auth.Subject; + +/** + * This class is heavily 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 DO_AS = lookupDoAs(); + private static final MethodHandle DO_PRIVILEGED = lookupDoPrivileged(); + private static final MethodHandle GET_CONTEXT = lookupGetContext(); + private static final MethodHandle GET_SUBJECT = lookupGetSubject(); + private static final MethodHandle CURRENT = lookupCurrent(); + + private static MethodHandle lookupDoAs() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + // Subject.doAs() is deprecated for removal and replaced by Subject.callAs(). + // 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, "callAs", + MethodType.methodType(Object.class, Subject.class, Callable.class)); + } catch (Throwable x) { + try { + // Lookup the old API. + MethodType oldSignature = + MethodType.methodType(Object.class, Subject.class, PrivilegedAction.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(PrivilegedAction.class, Callable.class); + MethodHandle converter = + lookup.findStatic(SecurityUtils.class, "callableToPrivilegedAction", + convertSignature); + return MethodHandles.filterArguments(doAs, 1, converter); + } catch (Throwable t) { + return null; + } + } + } + + 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 (Throwable x) { + 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 (Throwable x) { + try { + // This is a bit awkward, but the code is more symmetrical this way + return lookup.findStatic(SecurityUtils.class, "getSubjectFallback", + MethodType.methodType(Subject.class)); + } catch (Throwable t) { + return null; + } + } + } + + 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 (Throwable t) { + return null; + } + } + + 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 (Throwable x) { + return null; + } + } + + /** + * Get the current security manager, if available. + * @return the current security manager, if available + */ + public static Object getSecurityManager() { + try { + // Use reflection to work with Java versions that have and don't have SecurityManager. + return System.class.getMethod("getSecurityManager").invoke(null); + } catch (Throwable ignored) { + return null; + } + } + + /** + *

+ * Checks the given permission, if the {@link #getSecurityManager() security manager} is set. + *

+ * @param permission the permission to check + * @throws SecurityException if the permission check fails + */ + public static void checkPermission(Permission permission) throws SecurityException { + Object securityManager = SecurityUtils.getSecurityManager(); + if (securityManager == null) { + return; + } + try { + securityManager.getClass().getMethod("checkPermission").invoke(securityManager, + permission); + } catch (SecurityException x) { + throw x; + } catch (Throwable ignored) { + } + } + + /** + *

+ * Runs the given action with the calling context restricted to just the calling frame, not all + * the frames in the stack. + *

+ * @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. + MethodHandle methodHandle = DO_PRIVILEGED; + if (methodHandle == null) { + return action.run(); + } + return doPrivileged(methodHandle, action); + } + + @SuppressWarnings("unchecked") + private static T doPrivileged(MethodHandle doPrivileged, PrivilegedAction action) { + try { + return (T) doPrivileged.invoke(action); + } catch (RuntimeException | Error x) { + throw x; + } catch (Throwable x) { + throw new RuntimeException(x); + } + } + + /** + *

+ * Runs the given action as the given subject. + *

+ * @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 + */ + @SuppressWarnings("unchecked") + public static T callAs(Subject subject, Callable action) { + try { + MethodHandle methodHandle = DO_AS; + if (methodHandle == null) { + return action.call(); + } + return (T) methodHandle.invoke(subject, action); + } catch (RuntimeException | Error x) { + throw x; + } catch (Throwable x) { + throw new CompletionException(x); + } + } + + /** + *

+ * Gets the current subject + *

+ * @return the current subject + */ + @SuppressWarnings("unchecked") + public static Subject currentSubject() { + if (CURRENT == null) { + throw new RuntimeException( + "Was unable to run either of Subject.current() or Subject.getSubject()"); + } + try { + MethodHandle methodHandle = CURRENT; + return (Subject) methodHandle.invoke(); + } catch (Throwable x) { + throw new RuntimeException("Error when trying to get the current user", x); + } + + } + + private static PrivilegedAction callableToPrivilegedAction(Callable callable) { + return () -> { + try { + return callable.call(); + } catch (RuntimeException x) { + throw x; + } catch (Exception x) { + throw new RuntimeException(x); + } + }; + } + + private static Subject getSubjectFallback() { + try { + MethodHandle contextMethodHandle = GET_CONTEXT; + Object context = contextMethodHandle.invoke(); + + MethodHandle getSubjectMethodHandle = GET_SUBJECT; + return (Subject) getSubjectMethodHandle.invoke(context); + } catch (Throwable x) { + throw new RuntimeException("Error trying to get the current Subject", x); + } + } + + private SecurityUtils() { + } +} 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 ce9c2c9891..9c6b44cb2e 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 7a6d6b6841..164eef000c 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,14 +16,20 @@ */ 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; /** * Encapsulates creating the new Thread in a doPrivileged and a doAs call. * The doPrivilieged block is taken from Jetty, and prevents some classloader leak isses. + * + * Also according to Jetty, the referred leak was fixed in JDK17, and the doPriviliged block + * is no longer needed in 17 and later- + * * Saving the subject, and creating the Thread in the inner doAs call works around * doPriviliged resetting the kerberos subject, which breaks SPNEGO authentication. * @@ -39,12 +45,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 aeb63a16ce..1c831ee5b2 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 03857d8e4b..11e435530f 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 ad09b6f7db..8481971e21 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);