You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by ch...@apache.org on 2026/07/10 07:05:09 UTC
(kyuubi) branch master updated: [KYUUBI #7549] Fix Subject.getSubject() in Kyuubi JDBC driver for JDK 25 compatibility
This is an automated email from the ASF dual-hosted git repository.
pan3793 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git
The following commit(s) were added to refs/heads/master by this push:
new fe81544f64 [KYUUBI #7549] Fix Subject.getSubject() in Kyuubi JDBC driver for JDK 25 compatibility
fe81544f64 is described below
commit fe81544f64a495e4092933a487e96e8f8936b49a
Author: Fei Wang <fw...@ebay.com>
AuthorDate: Fri Jul 10 15:04:59 2026 +0800
[KYUUBI #7549] Fix Subject.getSubject() in Kyuubi JDBC driver for JDK 25 compatibility
## Why are the changes needed?
JDK 24 permanently disabled the Security Manager (JEP 486), making `AccessController.getContext()` throw `UnsupportedOperationException`. JDK 25 extended this to `Subject.getSubject()`. These APIs are called in the Kyuubi JDBC driver during Kerberos authentication, causing connections to fail:
```
java.lang.UnsupportedOperationException: getSubject is not supported
at java.base/javax.security.auth.Subject.getSubject(Subject.java)
```
Additionally, `Subject.doAs()` is deprecated for removal (JEP 411) and will break in a future JDK release.
Fixes #7549.
## What changed
Added `SubjectUtil` to `kyuubi-hive-jdbc`, ported from Apache Hadoop (HADOOP-19212, HADOOP-19906). It bridges the deprecated/removed Security Manager APIs to their JDK 18+ replacements using `MethodHandle` lookups resolved once at class load time:
- `Subject.getSubject(AccessController.getContext())` → `Subject.current()` (JDK 18+)
- `Subject.doAs()` → `Subject.callAs()` (JDK 18+)
- On JDK 22+, maintains an `InheritableThreadLocal<Subject>` to restore pre-JDK 22 Subject propagation into newly constructed threads (HADOOP-19906)
- Falls back to the old APIs on JDK < 18 transparently
Updated all affected callsites in the JDBC driver:
| File | Change |
|------|--------|
| `KyuubiConnection.java` | `Subject.getSubject()` × 2 → `SubjectUtil.current()` |
| `HttpAuthUtils.java` | `Subject.doAs()` → `SubjectUtil.doAs()` |
| `TSubjectTransport.java` | `Subject.doAs()` → `SubjectUtil.doAs()` |
## How was this patch tested?
Ported `TestSubjectUtil` from Apache Hadoop, covering exception propagation semantics of `callAs()`, `doAs(PrivilegedAction)`, `doAs(PrivilegedExceptionAction)`, and `sneakyThrow()` across JDK versions. Full `kyuubi-hive-jdbc` unit suite (43 tests) passes.
## Was this patch authored or co-authored using generative AI tooling?
Assisted-by: Claude:claude-sonnet-4-6
Closes #7550 from turboFei/jdk25.
Closes #7549
5baf2c974 [Fei Wang] refactor: move SubjectUtil to kyuubi-util; fix Javadoc and Kyuubi-managed comments
17d810697 [Fei Wang] test: replace SubjectUtilTest with full Hadoop TestSubjectUtil port
c01508255 [Fei Wang] fix: replace Subject.doAs() with SubjectUtil.doAs() in HttpAuthUtils and TSubjectTransport
727aa53d5 [Fei Wang] feat: replace SubjectUtil with full Hadoop implementation for JDK 25
fbfb88689 [Fei Wang] chore: remove docs/superpowers
27a02e8b2 [Fei Wang] style: reformat after JDK 25 Subject fix
492273c67 [Fei Wang] fix: replace Subject.getSubject() with SubjectUtil.current() for JDK 25 compat
78ba2f172 [Fei Wang] feat: add SubjectUtil to bridge Subject.getSubject() for JDK 25
fe909f976 [Fei Wang] docs: add JDK 25 Subject.getSubject fix implementation plan
23864a37e [Fei Wang] docs: add JDK 25 Subject.getSubject fix design spec
Authored-by: Fei Wang <fw...@ebay.com>
Signed-off-by: Cheng Pan <ch...@apache.org>
---
.../apache/kyuubi/jdbc/hive/KyuubiConnection.java | 6 +-
.../kyuubi/jdbc/hive/auth/HttpAuthUtils.java | 3 +-
.../kyuubi/jdbc/hive/auth/TSubjectTransport.java | 3 +-
.../java/org/apache/kyuubi/util/SubjectUtil.java | 303 ++++++++++++++++
.../org/apache/kyuubi/util/SubjectUtilTest.java | 394 +++++++++++++++++++++
5 files changed, 704 insertions(+), 5 deletions(-)
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java
index dc2b7afee3..50df116b02 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java
@@ -71,6 +71,7 @@ import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol;
import org.apache.kyuubi.shaded.thrift.transport.THttpClient;
import org.apache.kyuubi.shaded.thrift.transport.TTransport;
import org.apache.kyuubi.shaded.thrift.transport.TTransportException;
+import org.apache.kyuubi.util.SubjectUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -922,7 +923,7 @@ public class KyuubiConnection implements SQLConnection, KyuubiLoggable {
@SuppressWarnings("unchecked")
Class<? extends Principal> HadoopUserClz =
(Class<? extends Principal>) ClassUtils.getClass("org.apache.hadoop.security.User");
- Subject subject = Subject.getSubject(AccessController.getContext());
+ Subject subject = SubjectUtil.current();
return subject != null && !subject.getPrincipals(HadoopUserClz).isEmpty();
} catch (ClassNotFoundException e) {
return false;
@@ -1019,8 +1020,7 @@ public class KyuubiConnection implements SQLConnection, KyuubiLoggable {
String keytab = sessConfMap.get(AUTH_KYUUBI_CLIENT_KEYTAB);
return KerberosAuthenticationManager.getKeytabAuthentication(principal, keytab).getSubject();
} else if (isFromSubjectAuthMode()) {
- AccessControlContext context = AccessController.getContext();
- return Subject.getSubject(context);
+ return SubjectUtil.current();
} else if (isTgtCacheAuthMode()) {
String ticketCache = sessConfMap.getOrDefault(AUTH_KYUUBI_CLIENT_TICKET_CACHE, "");
return KerberosAuthenticationManager.getTgtCacheAuthentication(ticketCache).getSubject();
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java
index 0802e8effd..368bc9dadd 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java
@@ -20,6 +20,7 @@ package org.apache.kyuubi.jdbc.hive.auth;
import java.security.PrivilegedExceptionAction;
import java.util.Base64;
import javax.security.auth.Subject;
+import org.apache.kyuubi.util.SubjectUtil;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
@@ -35,7 +36,7 @@ public final class HttpAuthUtils {
public static String getKerberosServiceTicket(
String serverPrinciple, String host, Subject loggedInSubject) throws Exception {
String spn = KerberosUtils.canonicalPrincipal(serverPrinciple, host);
- return Subject.doAs(loggedInSubject, new HttpKerberosClientAction(spn));
+ return SubjectUtil.doAs(loggedInSubject, new HttpKerberosClientAction(spn));
}
private HttpAuthUtils() {
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java
index be7e581eef..e72f5f6c2f 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java
@@ -22,6 +22,7 @@ import java.security.PrivilegedExceptionAction;
import javax.security.auth.Subject;
import org.apache.kyuubi.shaded.thrift.transport.TTransport;
import org.apache.kyuubi.shaded.thrift.transport.TTransportException;
+import org.apache.kyuubi.util.SubjectUtil;
/**
* This is used on the client side, where the API explicitly opens transport to the server using the
@@ -39,7 +40,7 @@ public class TSubjectTransport extends TFilterTransport {
@Override
public void open() throws TTransportException {
try {
- Subject.doAs(
+ SubjectUtil.doAs(
subject,
(PrivilegedExceptionAction<Void>)
() -> {
diff --git a/kyuubi-util/src/main/java/org/apache/kyuubi/util/SubjectUtil.java b/kyuubi-util/src/main/java/org/apache/kyuubi/util/SubjectUtil.java
new file mode 100644
index 0000000000..e29359c317
--- /dev/null
+++ b/kyuubi-util/src/main/java/org/apache/kyuubi/util/SubjectUtil.java
@@ -0,0 +1,303 @@
+/*
+ * 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.kyuubi.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.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionException;
+import javax.security.auth.Subject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class that adapts Security Manager APIs across JDK versions (8 through 25+).
+ *
+ * <p>In JDK 17 the Security Manager and related APIs were deprecated for removal (JEP 411). In JDK
+ * 24 the Security Manager was permanently disabled (JEP 486). This class bridges {@code
+ * Subject.getSubject()}, {@code Subject.doAs()}, and {@code AccessController.getContext()} to their
+ * JDK 18+ replacements ({@code Subject.current()} and {@code Subject.callAs()}) using {@code
+ * MethodHandle} lookups resolved once at class load time.
+ *
+ * <p>Ported from Apache Hadoop 3.5.1 ({@code
+ * org.apache.hadoop.security.authentication.util.SubjectUtil}), HADOOP-19212 and HADOOP-19906,
+ * which is itself derived from Apache Calcite Avatica and the Jetty implementation.
+ */
+public final class SubjectUtil {
+ private static final Logger LOG = LoggerFactory.getLogger(SubjectUtil.class);
+
+ private static final MethodHandle CALL_AS = lookupCallAs();
+ static final boolean HAS_CALL_AS = CALL_AS != null;
+ private static final MethodHandle DO_AS = HAS_CALL_AS ? null : lookupDoAs();
+ private static final MethodHandle DO_AS_THROW_EXCEPTION =
+ HAS_CALL_AS ? null : lookupDoAsThrowException();
+ private static final MethodHandle CURRENT = lookupCurrent();
+
+ // "1.8" -> 8, "9" -> 9, "17" -> 17, etc.
+ private static final int JAVA_SPEC_VER =
+ Math.max(
+ 8, Integer.parseInt(System.getProperty("java.specification.version").split("\\.")[0]));
+
+ /** True if the current JVM copies the current JAAS subject into new threads automatically. */
+ public static final boolean THREAD_INHERITS_SUBJECT = checkThreadInheritsSubject();
+
+ /**
+ * Kyuubi-managed {@link InheritableThreadLocal} mirroring the active Subject for the duration of
+ * {@link #callAs} / {@link #doAs}. On JDK 22+ the JVM stopped propagating the Subject to newly
+ * constructed threads via {@code AccessControlContext} (JEP 411 / 486), and the new {@code
+ * ScopedValue}-based mechanism behind {@code Subject.current()} also does not inherit across
+ * {@code Thread.<init>}. Only consulted when {@link #THREAD_INHERITS_SUBJECT} is {@code false}.
+ */
+ private static final InheritableThreadLocal<Subject> CURRENT_SUBJECT_TL =
+ THREAD_INHERITS_SUBJECT ? null : new InheritableThreadLocal<>();
+
+ private static MethodHandle lookupCallAs() {
+ try {
+ return MethodHandles.lookup()
+ .findStatic(
+ Subject.class,
+ "callAs",
+ MethodType.methodType(Object.class, Subject.class, Callable.class));
+ } catch (NoSuchMethodException x) {
+ return null;
+ } catch (IllegalAccessException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static boolean checkThreadInheritsSubject() {
+ // JDK 22+ stopped propagating Subject to new threads via AccessControlContext.
+ // We accept the minor overhead for EOL 22/23 to avoid SecurityManager console warnings.
+ return JAVA_SPEC_VER <= 21;
+ }
+
+ private static MethodHandle lookupDoAs() {
+ try {
+ return MethodHandles.lookup()
+ .findStatic(
+ Subject.class,
+ "doAs",
+ MethodType.methodType(Object.class, Subject.class, PrivilegedAction.class));
+ } catch (IllegalAccessException | NoSuchMethodException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static MethodHandle lookupDoAsThrowException() {
+ try {
+ return MethodHandles.lookup()
+ .findStatic(
+ Subject.class,
+ "doAs",
+ MethodType.methodType(Object.class, Subject.class, PrivilegedExceptionAction.class));
+ } catch (IllegalAccessException | NoSuchMethodException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static MethodHandle lookupCurrent() {
+ try {
+ return MethodHandles.lookup()
+ .findStatic(Subject.class, "current", MethodType.methodType(Subject.class));
+ } catch (NoSuchMethodException e) {
+ // JDK < 18: compose AccessController.getContext() -> Subject.getSubject()
+ MethodHandle getContext = lookupGetContext();
+ MethodHandle getSubject = lookupGetSubject();
+ return MethodHandles.filterReturnValue(getContext, getSubject);
+ } catch (IllegalAccessException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static MethodHandle lookupGetSubject() {
+ try {
+ Class<?> contextKlass =
+ ClassLoader.getSystemClassLoader().loadClass("java.security.AccessControlContext");
+ return MethodHandles.lookup()
+ .findStatic(
+ Subject.class, "getSubject", MethodType.methodType(Subject.class, contextKlass));
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private static MethodHandle lookupGetContext() {
+ try {
+ Class<?> controllerKlass =
+ ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController");
+ Class<?> contextKlass =
+ ClassLoader.getSystemClassLoader().loadClass("java.security.AccessControlContext");
+ return MethodHandles.lookup()
+ .findStatic(controllerKlass, "getContext", MethodType.methodType(contextKlass));
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ /**
+ * Runs {@code action} as {@code subject}, bridging to {@code Subject.callAs()} on JDK 18+ and
+ * falling back to {@code Subject.doAs()} on older JDKs. On JDK 22+ also maintains {@link
+ * #CURRENT_SUBJECT_TL} so platform threads constructed inside {@code action} inherit the subject.
+ *
+ * @throws CompletionException wrapping any exception thrown by {@code action}
+ */
+ public static <T> T callAs(Subject subject, Callable<T> action) throws CompletionException {
+ Objects.requireNonNull(action);
+ if (THREAD_INHERITS_SUBJECT) {
+ return invokeCallAs(subject, action);
+ }
+ final Subject prev = CURRENT_SUBJECT_TL.get();
+ if (subject == null) {
+ CURRENT_SUBJECT_TL.remove();
+ } else if (subject != prev) {
+ CURRENT_SUBJECT_TL.set(subject);
+ }
+ try {
+ return invokeCallAs(subject, action);
+ } finally {
+ if (prev == null) {
+ CURRENT_SUBJECT_TL.remove();
+ } else if (prev != subject) {
+ CURRENT_SUBJECT_TL.set(prev);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> T invokeCallAs(Subject subject, Callable<T> action)
+ throws CompletionException {
+ if (HAS_CALL_AS) {
+ try {
+ return (T) CALL_AS.invoke(subject, action);
+ } catch (Throwable t) {
+ throw sneakyThrow(t);
+ }
+ } else {
+ try {
+ return doAs(subject, callableToPrivilegedAction(action));
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ }
+ }
+
+ /** Bridges to {@code Subject.callAs()} on JDK 18+ or {@code Subject.doAs()} on older JDKs. */
+ @SuppressWarnings("unchecked")
+ public static <T> T doAs(Subject subject, PrivilegedAction<T> action) {
+ Objects.requireNonNull(action);
+ if (HAS_CALL_AS) {
+ try {
+ return callAs(subject, privilegedActionToCallable(action));
+ } catch (CompletionException ce) {
+ Throwable cause = ce.getCause();
+ throw sneakyThrow(cause != null ? cause : ce);
+ }
+ } else {
+ try {
+ return (T) DO_AS.invoke(subject, action);
+ } catch (Throwable t) {
+ throw sneakyThrow(t);
+ }
+ }
+ }
+
+ /** Bridges to {@code Subject.callAs()} on JDK 18+ or {@code Subject.doAs()} on older JDKs. */
+ @SuppressWarnings("unchecked")
+ public static <T> T doAs(Subject subject, PrivilegedExceptionAction<T> action)
+ throws PrivilegedActionException {
+ Objects.requireNonNull(action);
+ if (HAS_CALL_AS) {
+ try {
+ return callAs(subject, privilegedExceptionActionToCallable(action));
+ } catch (CompletionException ce) {
+ Throwable cause = ce.getCause();
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else if (cause instanceof Exception) {
+ throw new PrivilegedActionException((Exception) cause);
+ } else {
+ throw sneakyThrow(cause);
+ }
+ }
+ } else {
+ try {
+ return (T) DO_AS_THROW_EXCEPTION.invoke(subject, action);
+ } catch (Throwable t) {
+ throw sneakyThrow(t);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link Subject} associated with the current execution context, or {@code null} if
+ * there is none. On JDK 18+ delegates to {@code Subject.current()}; on older JDKs delegates to
+ * {@code Subject.getSubject(AccessController.getContext())}. On JDK 22+ also consults {@link
+ * #CURRENT_SUBJECT_TL} to restore subject visibility for inherited platform threads.
+ */
+ public static Subject current() {
+ if (!THREAD_INHERITS_SUBJECT) {
+ Subject fromJdk = invokeJdkCurrent();
+ if (fromJdk != null) {
+ LOG.trace("Get current Subject from JDK API directly");
+ return fromJdk;
+ }
+ LOG.trace("Get current Subject from Kyuubi-managed InheritableThreadLocal");
+ return CURRENT_SUBJECT_TL.get();
+ }
+ return invokeJdkCurrent();
+ }
+
+ private static Subject invokeJdkCurrent() {
+ try {
+ return (Subject) CURRENT.invoke();
+ } catch (Throwable t) {
+ throw sneakyThrow(t);
+ }
+ }
+
+ private static <T> PrivilegedAction<T> callableToPrivilegedAction(Callable<T> callable) {
+ return () -> {
+ try {
+ return callable.call();
+ } catch (Exception e) {
+ throw sneakyThrow(e);
+ }
+ };
+ }
+
+ private static <T> Callable<T> privilegedExceptionActionToCallable(
+ PrivilegedExceptionAction<T> action) {
+ return action::run;
+ }
+
+ private static <T> Callable<T> privilegedActionToCallable(PrivilegedAction<T> action) {
+ return action::run;
+ }
+
+ @SuppressWarnings("unchecked")
+ static <E extends Throwable> RuntimeException sneakyThrow(Throwable e) throws E {
+ throw (E) e;
+ }
+
+ private SubjectUtil() {}
+}
diff --git a/kyuubi-util/src/test/java/org/apache/kyuubi/util/SubjectUtilTest.java b/kyuubi-util/src/test/java/org/apache/kyuubi/util/SubjectUtilTest.java
new file mode 100644
index 0000000000..db23aa7346
--- /dev/null
+++ b/kyuubi-util/src/test/java/org/apache/kyuubi/util/SubjectUtilTest.java
@@ -0,0 +1,394 @@
+/*
+ * 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.kyuubi.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionException;
+import org.junit.jupiter.api.Test;
+
+public class SubjectUtilTest {
+
+ // "1.8"->8, "9"->9, "10"->10
+ private static final int JAVA_SPEC_VER =
+ Math.max(
+ 8, Integer.parseInt(System.getProperty("java.specification.version").split("\\.")[0]));
+
+ @Test
+ void testHasCallAs() {
+ assertEquals(JAVA_SPEC_VER > 17, SubjectUtil.HAS_CALL_AS);
+ }
+
+ @Test
+ void testDoAsPrivilegedActionExceptionPropagation() {
+ // in Java 12 onwards, always throw the original exception thrown by action;
+ // in lower Java versions, throw a PrivilegedActionException that wraps the
+ // original exception when action throws a checked exception
+ Throwable e =
+ assertThrows(
+ Exception.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ RuntimeException innerE =
+ new RuntimeException("Inner Dummy RuntimeException");
+ throw SubjectUtil.sneakyThrow(new IOException("Dummy IOException", innerE));
+ }
+ }));
+ if (JAVA_SPEC_VER > 11) {
+ assertInstanceOf(IOException.class, e);
+ assertEquals("Dummy IOException", e.getMessage());
+ assertInstanceOf(RuntimeException.class, e.getCause());
+ assertEquals("Inner Dummy RuntimeException", e.getCause().getMessage());
+ assertNull(e.getCause().getCause());
+ } else {
+ assertInstanceOf(PrivilegedActionException.class, e);
+ assertNull(e.getMessage());
+ assertInstanceOf(IOException.class, e.getCause());
+ assertEquals("Dummy IOException", e.getCause().getMessage());
+ assertInstanceOf(RuntimeException.class, e.getCause().getCause());
+ assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage());
+ assertNull(e.getCause().getCause().getCause());
+ }
+
+ // same as above case because PrivilegedActionException is a checked exception
+ e =
+ assertThrows(
+ PrivilegedActionException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ throw SubjectUtil.sneakyThrow(new PrivilegedActionException(null));
+ }
+ }));
+ if (JAVA_SPEC_VER > 11) {
+ assertInstanceOf(PrivilegedActionException.class, e);
+ assertNull(e.getMessage());
+ assertNull(e.getCause());
+ } else {
+ assertInstanceOf(PrivilegedActionException.class, e);
+ assertNull(e.getMessage());
+ assertInstanceOf(PrivilegedActionException.class, e.getCause());
+ assertNull(e.getCause().getMessage());
+ assertNull(e.getCause().getCause());
+ }
+
+ // throw a RuntimeException directly when action throws a runtime exception
+ e =
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ throw new RuntimeException("Dummy RuntimeException");
+ }
+ }));
+ assertInstanceOf(RuntimeException.class, e);
+ assertEquals("Dummy RuntimeException", e.getMessage());
+ assertNull(e.getCause());
+
+ // same as above case because CompletionException is a runtime exception
+ e =
+ assertThrows(
+ CompletionException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ throw new CompletionException("Dummy CompletionException", null);
+ }
+ }));
+ assertInstanceOf(CompletionException.class, e);
+ assertEquals("Dummy CompletionException", e.getMessage());
+ assertNull(e.getCause());
+
+ // throw the original error when action throws an error
+ e =
+ assertThrows(
+ LinkageError.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ throw new LinkageError("Dummy LinkageError");
+ }
+ }));
+ assertInstanceOf(LinkageError.class, e);
+ assertEquals("Dummy LinkageError", e.getMessage());
+ assertNull(e.getCause());
+
+ // throw NPE when action is NULL
+ assertThrows(
+ NullPointerException.class,
+ () -> SubjectUtil.doAs(SubjectUtil.current(), (PrivilegedAction<Object>) null));
+ }
+
+ @Test
+ void testDoAsPrivilegedExceptionActionExceptionPropagation() {
+ // throw PrivilegedActionException that wraps the original exception when action throws
+ // a checked exception
+ Throwable e =
+ assertThrows(
+ PrivilegedActionException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedExceptionAction<Object>() {
+ @Override
+ public Object run() throws Exception {
+ RuntimeException innerE =
+ new RuntimeException("Inner Dummy RuntimeException");
+ throw new IOException("Dummy IOException", innerE);
+ }
+ }));
+ assertInstanceOf(PrivilegedActionException.class, e);
+ assertNull(e.getMessage());
+ assertInstanceOf(IOException.class, e.getCause());
+ assertEquals("Dummy IOException", e.getCause().getMessage());
+ assertInstanceOf(RuntimeException.class, e.getCause().getCause());
+ assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage());
+ assertNull(e.getCause().getCause().getCause());
+
+ // same as above because PrivilegedActionException is a checked exception
+ e =
+ assertThrows(
+ PrivilegedActionException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedExceptionAction<Object>() {
+ @Override
+ public Object run() throws Exception {
+ throw new PrivilegedActionException(null);
+ }
+ }));
+ assertInstanceOf(PrivilegedActionException.class, e);
+ assertNull(e.getMessage());
+ assertInstanceOf(PrivilegedActionException.class, e.getCause());
+ assertNull(e.getCause().getMessage());
+ assertNull(e.getCause().getCause());
+
+ // throw the original exception when action throws a runtime exception
+ e =
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedExceptionAction<Object>() {
+ @Override
+ public Object run() throws Exception {
+ throw new RuntimeException("Dummy RuntimeException");
+ }
+ }));
+ assertInstanceOf(RuntimeException.class, e);
+ assertEquals("Dummy RuntimeException", e.getMessage());
+ assertNull(e.getCause());
+
+ // same as above case because CompletionException is a runtime exception
+ e =
+ assertThrows(
+ CompletionException.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedExceptionAction<Object>() {
+ @Override
+ public Object run() throws Exception {
+ throw new CompletionException(null);
+ }
+ }));
+ assertInstanceOf(CompletionException.class, e);
+ assertNull(e.getMessage());
+ assertNull(e.getCause());
+
+ // throw the original error when action throws an error
+ e =
+ assertThrows(
+ LinkageError.class,
+ () ->
+ SubjectUtil.doAs(
+ SubjectUtil.current(),
+ new PrivilegedExceptionAction<Object>() {
+ @Override
+ public Object run() throws Exception {
+ throw new LinkageError("Dummy LinkageError");
+ }
+ }));
+ assertInstanceOf(LinkageError.class, e);
+ assertEquals("Dummy LinkageError", e.getMessage());
+ assertNull(e.getCause());
+
+ // throw NPE when action is NULL
+ assertThrows(
+ NullPointerException.class,
+ () -> SubjectUtil.doAs(SubjectUtil.current(), (PrivilegedExceptionAction<Object>) null));
+ }
+
+ @Test
+ void testCallAsExceptionPropagation() {
+ // always throw a CompletionException that wraps the original exception, when action throws
+ // a checked or runtime exception
+ Throwable e =
+ assertThrows(
+ CompletionException.class,
+ () ->
+ SubjectUtil.callAs(
+ SubjectUtil.current(),
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ RuntimeException innerE =
+ new RuntimeException("Inner Dummy RuntimeException");
+ throw new IOException("Dummy IOException", innerE);
+ }
+ }));
+ assertInstanceOf(CompletionException.class, e);
+ if (JAVA_SPEC_VER > 11) {
+ assertEquals("java.io.IOException: Dummy IOException", e.getMessage());
+ assertInstanceOf(IOException.class, e.getCause());
+ assertEquals("Dummy IOException", e.getCause().getMessage());
+ assertInstanceOf(RuntimeException.class, e.getCause().getCause());
+ assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage());
+ assertNull(e.getCause().getCause().getCause());
+ } else {
+ assertEquals(
+ "java.security.PrivilegedActionException: java.io.IOException: Dummy IOException",
+ e.getMessage());
+ assertInstanceOf(PrivilegedActionException.class, e.getCause());
+ assertNull(e.getCause().getMessage());
+ assertInstanceOf(IOException.class, e.getCause().getCause());
+ assertEquals("Dummy IOException", e.getCause().getCause().getMessage());
+ assertInstanceOf(RuntimeException.class, e.getCause().getCause().getCause());
+ assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getCause().getMessage());
+ assertNull(e.getCause().getCause().getCause().getCause());
+ }
+
+ e =
+ assertThrows(
+ CompletionException.class,
+ () ->
+ SubjectUtil.callAs(
+ SubjectUtil.current(),
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ throw new PrivilegedActionException(null);
+ }
+ }));
+ assertInstanceOf(CompletionException.class, e);
+ if (JAVA_SPEC_VER > 11) {
+ assertEquals("java.security.PrivilegedActionException", e.getMessage());
+ assertInstanceOf(PrivilegedActionException.class, e.getCause());
+ assertNull(e.getCause().getMessage());
+ assertNull(e.getCause().getCause());
+ } else {
+ assertEquals(
+ "java.security.PrivilegedActionException: java.security.PrivilegedActionException",
+ e.getMessage());
+ assertInstanceOf(PrivilegedActionException.class, e.getCause());
+ assertNull(e.getCause().getMessage());
+ assertInstanceOf(PrivilegedActionException.class, e.getCause().getCause());
+ assertNull(e.getCause().getCause().getMessage());
+ assertNull(e.getCause().getCause().getCause());
+ }
+
+ e =
+ assertThrows(
+ CompletionException.class,
+ () ->
+ SubjectUtil.callAs(
+ SubjectUtil.current(),
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ throw new RuntimeException("Dummy RuntimeException");
+ }
+ }));
+ assertInstanceOf(CompletionException.class, e);
+ assertEquals("java.lang.RuntimeException: Dummy RuntimeException", e.getMessage());
+ assertInstanceOf(RuntimeException.class, e.getCause());
+ assertEquals("Dummy RuntimeException", e.getCause().getMessage());
+ assertNull(e.getCause().getCause());
+
+ e =
+ assertThrows(
+ CompletionException.class,
+ () ->
+ SubjectUtil.callAs(
+ SubjectUtil.current(),
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ throw new CompletionException(null);
+ }
+ }));
+ assertInstanceOf(CompletionException.class, e);
+ assertEquals("java.util.concurrent.CompletionException", e.getMessage());
+ assertInstanceOf(CompletionException.class, e.getCause());
+ assertNull(e.getCause().getMessage());
+
+ // throw original error when action throws an error
+ e =
+ assertThrows(
+ LinkageError.class,
+ () ->
+ SubjectUtil.callAs(
+ SubjectUtil.current(),
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ throw new LinkageError("Dummy LinkageError");
+ }
+ }));
+ assertInstanceOf(LinkageError.class, e);
+ assertEquals("Dummy LinkageError", e.getMessage());
+ assertNull(e.getCause());
+
+ // throw NPE when action is NULL
+ assertThrows(NullPointerException.class, () -> SubjectUtil.callAs(SubjectUtil.current(), null));
+ }
+
+ @Test
+ void testSneakyThrow() {
+ IOException e = assertThrows(IOException.class, this::throwCheckedException);
+ assertEquals("Dummy IOException", e.getMessage());
+ }
+
+ private void throwCheckedException() {
+ throw SubjectUtil.sneakyThrow(new IOException("Dummy IOException"));
+ }
+}