diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java b/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java index 1099a15288..5fb215409b 100644 --- a/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/Negotiator.java @@ -32,15 +32,14 @@ import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.security.cert.Certificate; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; @@ -72,6 +71,7 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.Future; +import org.apache.kudu.client.internals.SecurityManagerCompatibility; import org.apache.yetus.audience.InterfaceAudience; import org.ietf.jgss.GSSException; import org.slf4j.Logger; @@ -952,14 +952,14 @@ private RpcOutboundMessage makeConnectionContext() throws SaslException { private byte[] evaluateChallenge(final byte[] challenge) throws SaslException, NonRecoverableException { try { - return Subject.doAs(securityContext.getSubject(), - new PrivilegedExceptionAction() { - @Override - public byte[] run() throws SaslException { - return saslClient.evaluateChallenge(challenge); - } - }); - } catch (PrivilegedActionException e) { + return SecurityManagerCompatibility.get().callAs(securityContext.getSubject(), + new Callable() { + @Override + public byte[] call() throws SaslException { + return saslClient.evaluateChallenge(challenge); + } + }); + } catch (CompletionException e) { // This cast is safe because the action above only throws checked SaslException. SaslException saslException = (SaslException) e.getCause(); @@ -1038,7 +1038,7 @@ public SharableSslHandler(SSLEngine engine) { } void resetAdded() { - Field addedField = AccessController.doPrivileged((PrivilegedAction) () -> { + Field addedField = SecurityManagerCompatibility.get().doPrivileged(() -> { try { Class c = ChannelHandlerAdapter.class; Field added = c.getDeclaredField("added"); diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java b/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java index cc2c8733d6..4bbdde01fd 100644 --- a/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/SecurityContext.java @@ -18,18 +18,15 @@ package org.apache.kudu.client; import java.io.IOException; -import java.security.AccessControlContext; -import java.security.AccessController; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nonnull; @@ -48,6 +45,7 @@ import com.google.common.collect.Lists; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import org.apache.kudu.client.internals.SecurityManagerCompatibility; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -154,8 +152,7 @@ private enum SubjectType { } private static Pair setupSubject() { - AccessControlContext context = AccessController.getContext(); - Subject subject = Subject.getSubject(context); + Subject subject = SecurityManagerCompatibility.get().current(); if (subject != null) { if (!subject.getPrincipals(KerberosPrincipal.class).isEmpty()) { LOG.debug("Using caller-provided subject with Kerberos principal {}. " + @@ -225,9 +222,9 @@ public void refreshSubject() { LOG.debug("Refreshing Kerberos credentials..."); Subject newSubject; try { - newSubject = Subject.doAs(new Subject(), - (PrivilegedExceptionAction) SecurityUtil::getSubjectFromTicketCacheOrNull); - } catch (PrivilegedActionException e) { + newSubject = SecurityManagerCompatibility.get().callAs(new Subject(), + SecurityUtil::getSubjectFromTicketCacheOrNull); + } catch (CompletionException e) { throw new RuntimeException(e.getCause()); } if (newSubject == null || SecurityUtil.getKerberosPrincipalOrNull(newSubject) == null) { diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/internals/CompositeStrategy.java b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/CompositeStrategy.java new file mode 100644 index 0000000000..2fd8fb7515 --- /dev/null +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/CompositeStrategy.java @@ -0,0 +1,107 @@ +/* + * 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.kudu.client.internals; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; + +import java.security.PrivilegedAction; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * This strategy combines the functionality of the {@link LegacyStrategy}, {@link ModernStrategy}, and + * {@link UnsupportedStrategy} strategies to provide the legacy APIs as long as they are present and not degraded. + * If the legacy APIs are missing or degraded, this falls back to the modern APIs. + */ +class CompositeStrategy + implements SecurityManagerCompatibility { + + private static final Logger log = LoggerFactory.getLogger(CompositeStrategy.class); + static final CompositeStrategy INSTANCE = new CompositeStrategy(ReflectiveStrategy.Loader.forName()); + + private final SecurityManagerCompatibility fallbackStrategy; + private final AtomicReference activeStrategy; + + // Visible for testing + CompositeStrategy(ReflectiveStrategy.Loader loader) { + SecurityManagerCompatibility initial; + SecurityManagerCompatibility fallback = null; + try { + initial = new LegacyStrategy(loader); + try { + fallback = new ModernStrategy(loader); + // This is expected for JRE 18+ + log.debug("Loaded legacy SecurityManager methods, will fall back to modern methods after UnsupportedOperationException"); + } catch (NoSuchMethodException | ClassNotFoundException ex) { + // This is expected for JRE <= 17 + log.debug("Unable to load modern Subject methods, relying only on legacy methods", ex); + } + } catch (ClassNotFoundException | NoSuchMethodException e) { + try { + initial = new ModernStrategy(loader); + // This is expected for JREs after the removal takes place. + log.debug("Unable to load legacy SecurityManager methods, relying only on modern methods", e); + } catch (NoSuchMethodException | ClassNotFoundException ex) { + initial = new UnsupportedStrategy(e, ex); + // This is not expected in normal use, only in test environments. + log.error("Unable to load legacy SecurityManager methods", e); + log.error("Unable to load modern Subject methods", ex); + } + } + Objects.requireNonNull(initial, "initial strategy must be defined"); + activeStrategy = new AtomicReference<>(initial); + fallbackStrategy = fallback; + } + + private T performAction(Function action) { + SecurityManagerCompatibility active = activeStrategy.get(); + try { + return action.apply(active); + } catch (UnsupportedOperationException e) { + // If we chose a fallback strategy during loading, switch to it and retry this operation. + if (active != fallbackStrategy && fallbackStrategy != null) { + if (activeStrategy.compareAndSet(active, fallbackStrategy)) { + log.debug("Using fallback strategy after encountering degraded legacy method", e); + } + return action.apply(fallbackStrategy); + } + // If we're already using the fallback strategy, then there's nothing to do to handle these exceptions. + throw e; + } + } + + @Override + public T doPrivileged(PrivilegedAction action) { + return performAction(compatibility -> compatibility.doPrivileged(action)); + } + + @Override + public Subject current() { + return performAction(SecurityManagerCompatibility::current); + } + + @Override + public T callAs(Subject subject, Callable action) throws CompletionException { + return performAction(compatibility -> compatibility.callAs(subject, action)); + } +} diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/internals/LegacyStrategy.java b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/LegacyStrategy.java new file mode 100644 index 0000000000..3743de086f --- /dev/null +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/LegacyStrategy.java @@ -0,0 +1,97 @@ +/* + * 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.kudu.client.internals; + +import javax.security.auth.Subject; + +import java.lang.reflect.Method; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +/** + * This class implements reflective access to the deprecated-for-removal methods of AccessController and Subject. + *

Instantiating this class may fail if any of the required classes or methods are not found. + * Method invocations for this class may fail with {@link UnsupportedOperationException} if all methods are found, + * but the operation is not permitted to be invoked. + *

This class is expected to be instantiable in JRE >=8 until the removal finally takes place. + */ +@SuppressWarnings("unchecked") +class LegacyStrategy + implements SecurityManagerCompatibility { + + private final Method doPrivileged; + private final Method getContext; + private final Method getSubject; + private final Method doAs; + + // Visible for testing + LegacyStrategy(ReflectiveStrategy.Loader loader) throws ClassNotFoundException, NoSuchMethodException { + Class accessController = loader.loadClass("java.security.AccessController"); + doPrivileged = accessController.getDeclaredMethod("doPrivileged", PrivilegedAction.class); + getContext = accessController.getDeclaredMethod("getContext"); + Class accessControlContext = loader.loadClass("java.security.AccessControlContext"); + Class subject = loader.loadClass(Subject.class.getName()); + getSubject = subject.getDeclaredMethod("getSubject", accessControlContext); + // Note that the Subject class isn't deprecated or removed, so reference it as an argument type. + // This allows for mocking out the method implementation while still accepting Subject instances as arguments. + doAs = subject.getDeclaredMethod("doAs", Subject.class, PrivilegedExceptionAction.class); + } + + @Override + public T doPrivileged(PrivilegedAction action) { + return (T) ReflectiveStrategy.invoke(doPrivileged, null, action); + } + + /** + * @return the result of AccessController.getContext(), of type AccessControlContext + */ + private Object getContext() { + return ReflectiveStrategy.invoke(getContext, null); + } + + /** + * @param context The current AccessControlContext + * @return The result of Subject.getSubject(AccessControlContext) + */ + private Subject getSubject(Object context) { + return (Subject) ReflectiveStrategy.invoke(getSubject, null, context); + } + + @Override + public Subject current() { + return getSubject(getContext()); + } + + /** + * @return The result of Subject.doAs(Subject, PrivilegedExceptionAction) + */ + private T doAs(Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { + return (T) ReflectiveStrategy.invokeChecked(doAs, PrivilegedActionException.class, null, subject, action); + } + + @Override + public T callAs(Subject subject, Callable callable) throws CompletionException { + try { + return doAs(subject, callable::call); + } catch (PrivilegedActionException e) { + throw new CompletionException(e.getCause()); + } + } +} diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/internals/ModernStrategy.java b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/ModernStrategy.java new file mode 100644 index 0000000000..314d932237 --- /dev/null +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/ModernStrategy.java @@ -0,0 +1,65 @@ +/* + * 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.kudu.client.internals; + +import javax.security.auth.Subject; + +import java.lang.reflect.Method; +import java.security.PrivilegedAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +/** + * This class implements reflective access to the methods of Subject added to replace deprecated methods. + *

Instantiating this class may fail if any of the required classes or methods are not found. + * Method invocations for this class may fail with {@link UnsupportedOperationException} if all methods are found, + * but the operation is not permitted to be invoked. + *

This class is expected to be instantiable in JRE >= 18. At the time of writing, these methods do not have + * a sunset date, and are expected to be available past the removal of the SecurityManager. + */ +@SuppressWarnings("unchecked") +class ModernStrategy + implements SecurityManagerCompatibility { + + private final Method current; + private final Method callAs; + + // Visible for testing + ModernStrategy(ReflectiveStrategy.Loader loader) throws NoSuchMethodException, ClassNotFoundException { + Class subject = loader.loadClass(Subject.class.getName()); + current = subject.getDeclaredMethod("current"); + // Note that the Subject class isn't deprecated or removed, so reference it as an argument type. + // This allows for mocking out the method implementation while still accepting Subject instances as arguments. + callAs = subject.getDeclaredMethod("callAs", Subject.class, Callable.class); + } + + @Override + public T doPrivileged(PrivilegedAction action) { + // This is intentionally a pass-through + return action.run(); + } + + @Override + public Subject current() { + return (Subject) ReflectiveStrategy.invoke(current, null); + } + + @Override + public T callAs(Subject subject, Callable action) throws CompletionException { + return (T) ReflectiveStrategy.invokeChecked(callAs, CompletionException.class, null, subject, action); + } +} diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/internals/ReflectiveStrategy.java b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/ReflectiveStrategy.java new file mode 100644 index 0000000000..3904ad44b5 --- /dev/null +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/ReflectiveStrategy.java @@ -0,0 +1,70 @@ +/* + * 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.kudu.client.internals; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Utility methods for strategies which use reflection to access methods without requiring them at compile-time. + */ +class ReflectiveStrategy +{ + + static Object invoke(Method method, Object obj, Object... args) { + try { + return method.invoke(obj, args); + } catch (IllegalAccessException e) { + throw new UnsupportedOperationException(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new RuntimeException(cause); + } + } + } + + static Object invokeChecked(Method method, Class ex, Object obj, Object... args) throws T { + try { + return method.invoke(obj, args); + } catch (IllegalAccessException e) { + throw new UnsupportedOperationException(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (ex.isInstance(cause)) { + throw ex.cast(cause); + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new RuntimeException(cause); + } + } + } + + /** + * Interface to allow mocking out classloading infrastructure. This is used to test reflective operations. + */ + interface Loader { + Class loadClass(String className) throws ClassNotFoundException; + + static Loader forName() { + return className -> Class.forName(className, true, Loader.class.getClassLoader()); + } + } +} diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/internals/SecurityManagerCompatibility.java b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/SecurityManagerCompatibility.java new file mode 100644 index 0000000000..6a9ead8c0c --- /dev/null +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/SecurityManagerCompatibility.java @@ -0,0 +1,103 @@ +/* + * 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.kudu.client.internals; + +import javax.security.auth.Subject; + +import java.security.PrivilegedAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +/** + * This is a compatibility class to provide dual-support for JREs with and without SecurityManager support. + *

Users should call {@link #get()} to retrieve a singleton instance, and call instance methods + * {@link #doPrivileged(PrivilegedAction)}, {@link #current()}, and {@link #callAs(Subject, Callable)}. + *

This class's motivation and expected behavior is defined in + * KIP-1006 + */ +public interface SecurityManagerCompatibility +{ + + /** + * @return an implementation of this interface which conforms to the functionality available in the current JRE. + */ + static SecurityManagerCompatibility get() { + return CompositeStrategy.INSTANCE; + } + + /** + * Performs the specified {@code PrivilegedAction} with privileges + * enabled. The action is performed with all of the permissions + * possessed by the caller's protection domain. + * + *

If the action's {@code run} method throws an (unchecked) + * exception, it will propagate through this method. + * + *

Note that any DomainCombiner associated with the current + * AccessControlContext will be ignored while the action is performed. + * + * @param the type of the value returned by the PrivilegedAction's + * {@code run} method. + * + * @param action the action to be performed. + * + * @return the value returned by the action's {@code run} method. + * + * @exception NullPointerException if the action is {@code null} + * @see java.security.AccessController#doPrivileged(PrivilegedAction) + */ + T doPrivileged(PrivilegedAction action); + + /** + * Returns the current subject. + *

+ * The current subject is installed by the {@link #callAs} method. + * When {@code callAs(subject, action)} is called, {@code action} is + * executed with {@code subject} as its current subject which can be + * retrieved by this method. After {@code action} is finished, the current + * subject is reset to its previous value. The current + * subject is {@code null} before the first call of {@code callAs()}. + * + * @return the current subject, or {@code null} if a current subject is + * not installed or the current subject is set to {@code null}. + * @see #callAs(Subject, Callable) + * @see Subject#current() + * @see Subject#callAs(Subject, Callable) + */ + Subject current(); + + /** + * Executes a {@code Callable} with {@code subject} as the + * current subject. + * + * @param subject the {@code Subject} that the specified {@code action} + * will run as. This parameter may be {@code null}. + * @param action the code to be run with {@code subject} as its current + * subject. Must not be {@code null}. + * @param the type of value returned by the {@code call} method + * of {@code action} + * @return the value returned by the {@code call} method of {@code action} + * @throws NullPointerException if {@code action} is {@code null} + * @throws CompletionException if {@code action.call()} throws an exception. + * The cause of the {@code CompletionException} is set to the exception + * thrown by {@code action.call()}. + * @see #current() + * @see Subject#current() + * @see Subject#callAs(Subject, Callable) + */ + T callAs(Subject subject, Callable action) throws CompletionException; +} diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/internals/UnsupportedStrategy.java b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/UnsupportedStrategy.java new file mode 100644 index 0000000000..23013e0cf9 --- /dev/null +++ b/java/kudu-client/src/main/java/org/apache/kudu/client/internals/UnsupportedStrategy.java @@ -0,0 +1,61 @@ +/* + * 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.kudu.client.internals; + +import javax.security.auth.Subject; + +import java.security.PrivilegedAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +/** + * This is a fallback strategy to use if no other strategies are available. + *

This is used to improve control flow and provide detailed error messages in unusual situations. + */ +class UnsupportedStrategy + implements SecurityManagerCompatibility { + + private final Throwable e1; + private final Throwable e2; + + UnsupportedStrategy(Throwable e1, Throwable e2) { + this.e1 = e1; + this.e2 = e2; + } + + private UnsupportedOperationException createException(String message) { + UnsupportedOperationException e = new UnsupportedOperationException(message); + e.addSuppressed(e1); + e.addSuppressed(e2); + return e; + } + + @Override + public T doPrivileged(PrivilegedAction action) { + throw createException("Unable to find suitable AccessController#doPrivileged implementation"); + } + + @Override + public Subject current() { + throw createException("Unable to find suitable Subject#getCurrent or Subject#current implementation"); + } + + @Override + public T callAs(Subject subject, Callable action) throws CompletionException { + throw createException("Unable to find suitable Subject#doAs or Subject#callAs implementation"); + } +}