This is an automated email from the ASF dual-hosted git repository.

stoty pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git


The following commit(s) were added to refs/heads/main by this push:
     new b656437ad [CALCITE-6590] Use reflection to handle Java SecurityManager 
deprecation in Avatica
b656437ad is described below

commit b656437ad924c349bd225caa089749ac913cb4ff
Author: Istvan Toth <[email protected]>
AuthorDate: Tue Sep 24 13:21:39 2024 +0200

    [CALCITE-6590] Use reflection to handle Java SecurityManager deprecation in 
Avatica
    
    Co-authored-by: Chris Dennis <[email protected]>
---
 .../avatica/remote/DoAsAvaticaHttpClient.java      |  12 +-
 .../calcite/avatica/remote/KerberosConnection.java |   2 +-
 .../apache/calcite/avatica/util/SecurityUtils.java | 203 +++++++++++++++++++++
 .../apache/calcite/avatica/server/HttpServer.java  |   6 +-
 .../SubjectPreservingPrivilegedThreadFactory.java  |  13 +-
 .../apache/calcite/avatica/AvaticaSpnegoTest.java  |   7 +-
 .../org/apache/calcite/avatica/SpnegoTestUtil.java |   7 +-
 .../server/HttpServerSpnegoWithoutJaasTest.java    |   7 +-
 8 files changed, 234 insertions(+), 23 deletions(-)

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<byte[]>() {
-      @Override public byte[] run() {
+  @Override
+  public byte[] send(final byte[] request) {
+    return SecurityUtils.callAs(kerberosUtil.getSubject(), new 
Callable<byte[]>() {
+      @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<RenewalTask, Thread> 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
+ *
+ * <p>Collections of utility methods to deal with the scheduled removal
+ * of the security classes defined by <a 
href="https://openjdk.org/jeps/411";>JEP 411</a>.</p>
+ */
+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 <T> the type of the result
+   */
+  public static <T> T doPrivileged(PrivilegedAction<T> action) {
+    // Keep this method short and inlineable.
+    if (DO_PRIVILEGED == null) {
+      return action.run();
+    }
+    return doPrivileged(DO_PRIVILEGED, action);
+  }
+
+  private static <T> T doPrivileged(MethodHandle doPrivileged, 
PrivilegedAction<T> 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 <T> the type of the result
+   * @throws CompletionException
+   */
+  public static <T> T callAs(Subject subject, Callable<T> 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 <T> PrivilegedExceptionAction<T> 
callableToPrivilegedExceptionAction(
+      Callable<T> callable) {
+    return callable::call;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <E extends Throwable> 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<Void>() {
-        @Override public Void run() {
+      SecurityUtils.callAs(subject, new Callable<Void>() {
+        @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<Thread>() {
+    Subject subject = SecurityUtils.currentSubject();
+    return SecurityUtils.doPrivileged(new PrivilegedAction<Thread>() {
       @Override public Thread run() {
-        return Subject.doAs(subject, new PrivilegedAction<Thread>() {
-          @Override public Thread run() {
+        return SecurityUtils.callAs(subject, new Callable<Thread>() {
+          @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<Void>() {
-        @Override public Void run() throws Exception {
+      SecurityUtils.callAs(clientSubject, new Callable<Void>() {
+        @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<Configuration>() {
-      public Configuration run() {
+    SecurityUtils.doPrivileged(new PrivilegedAction<Configuration>() {
+      @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<byte[]>() {
-      @Override public byte[] run() throws Exception {
+    byte[] response = SecurityUtils.callAs(clientSubject, new 
Callable<byte[]>() {
+      @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);

Reply via email to