diff --git a/gateway-provider-security-shiro/pom.xml b/gateway-provider-security-shiro/pom.xml index 8cce8d339b..23a36d3d3b 100644 --- a/gateway-provider-security-shiro/pom.xml +++ b/gateway-provider-security-shiro/pom.xml @@ -126,5 +126,10 @@ gateway-test-utils test + + + de.thetaphi + forbiddenapis + - + \ No newline at end of file diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java index 0f8ef22b0f..c476f98b76 100644 --- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java +++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java @@ -17,6 +17,7 @@ */ package org.apache.knox.gateway.filter; +import de.thetaphi.forbiddenapis.SuppressForbidden; import org.apache.commons.lang3.StringUtils; import org.apache.knox.gateway.ShiroMessages; import org.apache.knox.gateway.audit.api.Action; @@ -44,6 +45,8 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URISyntaxException; import java.security.Principal; import java.security.PrivilegedExceptionAction; @@ -71,6 +74,49 @@ public class ShiroSubjectIdentityAdapter implements Filter { /* List of URLs with anon authentication */ private static List anonUrls = new ArrayList<>(); + /* + * Subject.callAs(Subject, Callable) was introduced in JDK 18 (JEP 411). + * Resolved once at class-load time to avoid per-request reflection overhead. + * On JDK 17, SUBJECT_CALL_AS will be null and doSubjectAction() falls back + * to Subject.doAs() which still works on JDK 17. + * On JDK 23+, Subject.doAs() throws UnsupportedOperationException (KNOX-3338). + */ + private static final Method SUBJECT_CALL_AS; + + static { + Method m = null; + try { + m = javax.security.auth.Subject.class.getMethod( + "callAs", javax.security.auth.Subject.class, Callable.class); + } catch (NoSuchMethodException | SecurityException e) { + // JDK 17 — will fall back to Subject.doAs() in doSubjectAction() + } + SUBJECT_CALL_AS = m; + } + + /** + * Executes action under the given subject's identity. + * Uses Subject.callAs() on JDK 18+, falls back to Subject.doAs() on JDK 17. + * Fixes UnsupportedOperationException on JDK 23+ (KNOX-3338). + */ + @SuppressForbidden + private static void doSubjectAction(javax.security.auth.Subject subject, + Callable action) throws Exception { + if (SUBJECT_CALL_AS != null) { + try { + SUBJECT_CALL_AS.invoke(null, subject, action); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { throw (Exception) cause; } + throw new RuntimeException(cause); + } + } else { + // JDK 17 fallback — deprecated but functional on JDK 17 + javax.security.auth.Subject.doAs(subject, + (PrivilegedExceptionAction) action::call); + } + } + @Override public void init( FilterConfig filterConfig ) throws ServletException { /* Create a shiro urls config map */ @@ -130,13 +176,11 @@ private static class CallableChain implements Callable { @SuppressWarnings("unchecked") @Override public Void call() throws Exception { - PrivilegedExceptionAction action = new PrivilegedExceptionAction() { - @Override - public Void run() throws Exception { + Callable action = () -> { chain.doFilter( request, response ); return null; - } }; + Subject shiroSubject = SecurityUtils.getSubject(); /** @@ -158,7 +202,7 @@ public Void run() throws Exception { AuditContext context = auditService.getContext(); context.setUsername(principal); auditService.attachContext(context); - javax.security.auth.Subject.doAs(subject, action); + doSubjectAction(subject, action); } else { final String principal = shiroSubject.getPrincipal().toString(); @@ -211,7 +255,7 @@ public Void run() throws Exception { // To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials"). javax.security.auth.Subject subject = new javax.security.auth.Subject( true, principals, Collections.emptySet(), Collections.emptySet()); - javax.security.auth.Subject.doAs(subject, action); + doSubjectAction(subject, action); } return null; diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java index 9b6b50a62e..5d0ffd9f2f 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/SubjectUtils.java @@ -22,6 +22,7 @@ import javax.security.auth.Subject; import java.security.AccessController; +import java.lang.reflect.Method; import java.security.Principal; import java.util.Collections; import java.util.Optional; @@ -36,9 +37,36 @@ public class SubjectUtils { * There is no option in JDK 17 other then suppressing. * For JDK 18+ use Subject.current() instead. */ + /* + * Subject.current() was introduced in JDK 18 (JEP 411). + * Resolved once at class-load time to avoid per-request reflection overhead. + * On JDK 17, SUBJECT_CURRENT_METHOD will be null and we fall back to + * the deprecated Subject.getSubject() which still works on JDK 17. + * On JDK 23+, the fallback throws UnsupportedOperationException (KNOX-3338). + */ + private static final Method SUBJECT_CURRENT_METHOD; + + static { + Method m = null; + try { + m = Subject.class.getMethod("current"); // available since JDK 18 + } catch (NoSuchMethodException | SecurityException e) { + // JDK 17 — fallback to Subject.getSubject() below + } + SUBJECT_CURRENT_METHOD = m; + } + @SuppressForbidden public static Subject getCurrentSubject() { - return Subject.getSubject( AccessController.getContext() ); + if (SUBJECT_CURRENT_METHOD != null) { + try { + return (Subject) SUBJECT_CURRENT_METHOD.invoke(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Subject.current() invocation failed", e); + } + } + // JDK 17 fallback — deprecated but functional; throws on JDK 23+ + return Subject.getSubject(AccessController.getContext()); } public static String getPrimaryPrincipalName(Subject subject) {