problemList;
+ int emittedClassFileCount;
private final String className;
private final char[] sourceCode;
@@ -602,6 +658,7 @@ public void acceptResult(CompilationResult result)
else
{
ClassFile[] classFiles = result.getClassFiles();
+ emittedClassFileCount += classFiles.length;
for (ClassFile classFile : classFiles)
targetClassLoader.addClass(className, classFile.getBytes());
}
diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
index 71d51c09c9e6..68e067f7951d 100644
--- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
+++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java
@@ -152,6 +152,13 @@ public abstract class UDFunction extends UserFunction implements ScalarFunction
"java/lang/Thread.class",
"java/lang/ThreadGroup.class",
"java/lang/ThreadLocal.class",
+ // Block classes under allowed package prefixes that expose restricted operations.
+ // The class loader must resolve Module because Class references it.
+ // The bytecode verifier blocks Module and ModuleLayer calls.
+ "java/lang/ProcessHandle", // Provides process access on Java Development Kit (JDK) 9 and later.
+ "java/lang/StackWalker", // JDK 9 stack and class access
+ "java/lang/foreign/", // JDK 22 native calls and memory access
+ "java/lang/classfile/", // JDK 24 class-file generation and parsing
"java/lang/instrument/",
"java/lang/invoke/",
"java/lang/management/",
diff --git a/src/java/org/apache/cassandra/security/JMXSubjects.java b/src/java/org/apache/cassandra/security/JMXSubjects.java
new file mode 100644
index 000000000000..511bee38616a
--- /dev/null
+++ b/src/java/org/apache/cassandra/security/JMXSubjects.java
@@ -0,0 +1,81 @@
+/*
+ * 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.cassandra.security;
+
+import java.lang.reflect.Method;
+import java.security.AccessController;
+
+import javax.security.auth.Subject;
+
+/**
+ * Gets the {@link Subject} for the current Java Management Extensions (JMX) call.
+ *
+ * Java Development Kit (JDK) 11, 17, and 21 store the subject in the {@link java.security.AccessControlContext}.
+ * Cassandra reads it with {@code Subject.getSubject}.
+ *
+ * On JDK 24 and later, Cassandra calls {@code Subject.current}.
+ * It uses reflection to keep compilation compatible with JDK 11.
+ */
+public final class JMXSubjects
+{
+ private static final Method SUBJECT_CURRENT = subjectCurrentMethod();
+
+ private JMXSubjects()
+ {
+ }
+
+ private static Method subjectCurrentMethod()
+ {
+ try
+ {
+ return Subject.class.getMethod("current");
+ }
+ catch (NoSuchMethodException e)
+ {
+ return null; // Subject.current is available on JDK 18 and later.
+ }
+ }
+
+ /**
+ * @return the {@link Subject} associated with the current JMX invocation, or {@code null} if there is none.
+ */
+ @SuppressWarnings({ "deprecation", "removal" })
+ public static Subject current()
+ {
+ if (ThreadAwareSecurityManager.isSecurityManagerSupported())
+ return Subject.getSubject(AccessController.getContext());
+
+ return currentSubject();
+ }
+
+ static Subject currentSubject()
+ {
+ if (SUBJECT_CURRENT == null)
+ throw new IllegalStateException("Subject.current() is unavailable but a SecurityManager is not supported on this JVM");
+
+ try
+ {
+ return (Subject) SUBJECT_CURRENT.invoke(null);
+ }
+ catch (ReflectiveOperationException e)
+ {
+ throw new RuntimeException("Failed to invoke Subject.current()", e);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java b/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java
index 8639b46ffb3f..b23d7d946a92 100644
--- a/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java
+++ b/src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java
@@ -34,10 +34,13 @@
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.logging.LoggingSupportFactory;
import io.netty.util.concurrent.FastThreadLocal;
+import static org.apache.cassandra.config.CassandraRelevantProperties.UDF_SECURITY_MECHANISM;
+
/**
* Custom {@link SecurityManager} and {@link Policy} implementation that only performs access checks
* if explicitly enabled.
@@ -80,24 +83,86 @@ public Enumeration elements()
private static volatile boolean installed;
- public static void install()
+ /**
+ * Returns whether the running Java Development Kit (JDK) supports {@link SecurityManager} installation.
+ * On JDK 24 and later, {@code System.setSecurityManager} throws {@link UnsupportedOperationException}.
+ */
+ public static boolean isSecurityManagerSupported()
+ {
+ return Runtime.version().feature() < 24;
+ }
+
+ /**
+ * Returns whether the configured user-defined function (UDF) sandbox uses a {@link SecurityManager}.
+ * {@code auto} selects it before JDK 24.
+ * {@code sandbox} disables it.
+ * {@code securitymanager} requires it.
+ * With {@code securitymanager}, {@link #install()} fails on JDK 24 and later.
+ */
+ public static boolean useSecurityManager()
+ {
+ return useSecurityManager(UDF_SECURITY_MECHANISM.getString(), Runtime.version().feature());
+ }
+
+ static boolean useSecurityManager(String value, int javaVersion)
+ {
+ String mechanism = value.trim();
+ if (mechanism.equalsIgnoreCase("securitymanager"))
+ return true;
+ if (mechanism.equalsIgnoreCase("sandbox"))
+ return false;
+ if (mechanism.equalsIgnoreCase("auto"))
+ return javaVersion < 24;
+ throw new ConfigurationException(String.format("Invalid value '%s' for %s; expected one of: auto, securitymanager, sandbox",
+ value, UDF_SECURITY_MECHANISM.getKey()));
+ }
+
+ public static synchronized void install()
{
+ boolean useSecurityManager = useSecurityManager();
+ if (!useSecurityManager)
+ {
+ // The byte-code sandbox does not require SecurityManager installation.
+ logger.info("Using the SecurityManager-free UDF sandbox (Java {}, {}={}).",
+ Runtime.version().feature(), UDF_SECURITY_MECHANISM.getKey(), UDF_SECURITY_MECHANISM.getString());
+ }
+
+ if (useSecurityManager && !isSecurityManagerSupported())
+ {
+ // Reject an unsupported SecurityManager request before UDFs can run.
+ throw new ConfigurationException(String.format("%s=securitymanager but a SecurityManager cannot be installed on Java %d. " +
+ "Use 'auto' or 'sandbox' to use the SecurityManager-free UDF sandbox.",
+ UDF_SECURITY_MECHANISM.getKey(), Runtime.version().feature()));
+ }
+
if (installed)
return;
- // this line is needed - we need to make sure AccessControlException is loaded before we install this SM
- // otherwise we may get into stackoverflow when javax.security is not allowed package, and ACE is tried to be
- // loaded when it is going to be thrown from SM (class loader triggers SM to verify javax.security,
- // it recognizes it as not allowed and attempts to throw it...)
- //noinspection PlaceholderCountMatchesArgumentCount
- logger.trace("Initialized thread aware security manager", AccessControlException.class.getName());
+ if (useSecurityManager)
+ {
+ // Load AccessControlException before installing the security manager.
+ // Loading it during a permission check can cause recursion.
+ //noinspection PlaceholderCountMatchesArgumentCount
+ logger.trace("Initialized thread aware security manager", AccessControlException.class.getName());
- System.setSecurityManager(new ThreadAwareSecurityManager());
+ try
+ {
+ installLegacyPolicy();
+ System.setSecurityManager(new ThreadAwareSecurityManager());
+ }
+ catch (UnsupportedOperationException | SecurityException e)
+ {
+ throw new ConfigurationException("Cannot install the UDF security manager. Set " +
+ UDF_SECURITY_MECHANISM.getKey() + "=sandbox to use the bytecode sandbox.", e);
+ }
+ }
LoggingSupportFactory.getLoggingSupport().onStartup();
installed = true;
}
- static
+ /** Installs the policy only when the selected mechanism requires a security manager. */
+ @SuppressWarnings("removal")
+ private static void installLegacyPolicy()
{
//
// Use own security policy to be easier (and faster) since the C* has no fine grained permissions.
diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java b/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
index a7cf62504d5d..89567b686f5f 100644
--- a/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
+++ b/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
@@ -24,16 +24,21 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.server.RMISocketFactory;
+import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import javax.management.InstanceNotFoundException;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
+import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
+import javax.management.remote.MBeanServerForwarder;
+import javax.security.auth.Subject;
import com.datastax.driver.core.BatchStatement;
import com.datastax.driver.core.PreparedStatement;
@@ -55,6 +60,7 @@
import org.apache.cassandra.auth.AuthEvents;
import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.CassandraPrincipal;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.JMXResource;
import org.apache.cassandra.auth.Permission;
@@ -840,6 +846,47 @@ public void testJMXArchiveCommand() throws IOException
assertEquals("/xyz/not/null", AuditLogManager.instance.getAuditLogOptions().archive_command);
}
+ @Test
+ public void testJMXHandlerSubject() throws Exception
+ {
+ Subject authenticated = new Subject();
+ authenticated.getPrincipals().add(new CassandraPrincipal("audit_role"));
+ ObjectName missing = new ObjectName("subject-test:type=Missing");
+ AuditLogManager manager = AuditLogManager.instance;
+ manager.resetMBeanServerForwarder();
+ try
+ {
+ MBeanServerForwarder forwarder = manager.getMBeanServerForwarder();
+ forwarder.setMBeanServer(MBeanServerFactory.newMBeanServer("subject-test"));
+ InMemoryAuditLogger logger = (InMemoryAuditLogger) manager.getLogger();
+ logger.inMemQueue.clear();
+ for (Subject subject : new Subject[]{ authenticated, null })
+ {
+ Subject.doAs(subject, (PrivilegedAction) () -> {
+ assertEquals("subject-test", forwarder.getDefaultDomain());
+ Assertions.assertThatThrownBy(() -> forwarder.getAttribute(missing, "Missing"))
+ .isInstanceOf(InstanceNotFoundException.class);
+ return null;
+ });
+
+ String user = subject == null ? "null" : "CassandraPrincipal: audit_role";
+ AuditLogEntry success = logger.inMemQueue.remove();
+ assertEquals(AuditLogEntryType.JMX, success.getType());
+ assertEquals(user, success.getUser());
+ assertThat(success.getOperation(), stringContainsInOrder("JMX INVOCATION", "getDefaultDomain"));
+ AuditLogEntry failure = logger.inMemQueue.remove();
+ assertEquals(AuditLogEntryType.JMX, failure.getType());
+ assertEquals(user, failure.getUser());
+ assertThat(failure.getOperation(), stringContainsInOrder("JMX FAILURE", "getAttribute"));
+ assertTrue(logger.inMemQueue.isEmpty());
+ }
+ }
+ finally
+ {
+ manager.resetMBeanServerForwarder();
+ }
+ }
+
@Test
public void testJMXAuditLogs() throws Throwable
{
diff --git a/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java b/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
index f7ab44b671be..26117d8983ff 100644
--- a/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
+++ b/test/unit/org/apache/cassandra/auth/jmx/AuthorizationProxyTest.java
@@ -18,6 +18,9 @@
package org.apache.cassandra.auth.jmx;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@@ -28,8 +31,12 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanServer;
+import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
+import javax.management.remote.MBeanServerForwarder;
import javax.security.auth.Subject;
import com.google.common.collect.ImmutableMap;
@@ -45,11 +52,20 @@
import org.apache.cassandra.auth.PermissionDetails;
import org.apache.cassandra.auth.RoleResource;
import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.utils.JmxInvocationListener;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
public class AuthorizationProxyTest
{
@@ -71,6 +87,79 @@ public static void setup() throws Exception
RoleResource role1 = RoleResource.role("r1");
+ @Test
+ public void invocationUsesAuthenticatedSubject() throws Exception
+ {
+ Subject allowed = subject(role1.getRoleName());
+ Subject denied = subject("denied");
+ AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+ .isSuperuser(role -> false)
+ .getPermissions(role -> role.equals(role1)
+ ? Collections.singleton(permission(role1, JMXResource.root(), Permission.DESCRIBE))
+ : Collections.emptySet())
+ .build();
+ JmxInvocationListener listener = mock(JmxInvocationListener.class);
+ proxy.listener = listener;
+ MBeanServerForwarder forwarder = forwarder(proxy);
+ Method method = MBeanServer.class.getMethod("getDefaultDomain");
+
+ ObjectName missing = new ObjectName("subject-test:type=Missing");
+ Subject.doAs(allowed, (PrivilegedAction) () -> {
+ assertEquals("subject-test", forwarder.getDefaultDomain());
+ assertThatThrownBy(() -> forwarder.getMBeanInfo(missing)).isInstanceOf(InstanceNotFoundException.class);
+ Subject.doAs(denied, (PrivilegedAction) () -> {
+ assertThatThrownBy(forwarder::getDefaultDomain).isInstanceOf(SecurityException.class);
+ return null;
+ });
+ return null;
+ });
+
+ verify(listener).onInvocation(same(allowed), eq(method), isNull());
+ verify(listener).onFailure(same(allowed),
+ eq(MBeanServer.class.getMethod("getMBeanInfo", ObjectName.class)),
+ eq(new Object[]{ missing }),
+ any(InstanceNotFoundException.class));
+ verify(listener).onFailure(same(denied), eq(method), isNull(), any(SecurityException.class));
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void invocationWithoutSubjectPreservesConnectorAuthorization() throws Exception
+ {
+ AuthorizationProxy proxy = new ProxyBuilder().isAuthzRequired(() -> true)
+ .isSuperuser(role -> {
+ throw new AssertionError("A connector invocation must not check a role");
+ })
+ .build();
+ JmxInvocationListener listener = mock(JmxInvocationListener.class);
+ proxy.listener = listener;
+ MBeanServerForwarder forwarder = forwarder(proxy);
+ Method method = MBeanServer.class.getMethod("getDefaultDomain");
+
+ Subject.doAs(null, (PrivilegedAction) () -> {
+ assertEquals("subject-test", forwarder.getDefaultDomain());
+ proxy.isAuthSetupComplete = () -> false;
+ assertThatThrownBy(forwarder::getDefaultDomain).isInstanceOf(SecurityException.class);
+ return null;
+ });
+
+ verify(listener).onInvocation(isNull(), eq(method), isNull());
+ verify(listener).onFailure(isNull(), eq(method), isNull(), any(SecurityException.class));
+ verifyNoMoreInteractions(listener);
+ }
+
+ private static MBeanServerForwarder forwarder(AuthorizationProxy proxy)
+ {
+ MBeanServerForwarder forwarder = (MBeanServerForwarder) Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(),
+ new Class>[]{ MBeanServerForwarder.class },
+ proxy);
+ Subject.doAs(null, (PrivilegedAction) () -> {
+ forwarder.setMBeanServer(MBeanServerFactory.newMBeanServer("subject-test"));
+ return null;
+ });
+ return forwarder;
+ }
+
@Test
public void roleHasRequiredPermission() throws Throwable
{
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFInsecureSecurityManagerTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFInsecureSecurityManagerTest.java
new file mode 100644
index 000000000000..61033e08edc3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFInsecureSecurityManagerTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.cassandra.cql3.validation.entities;
+
+import org.junit.Assume;
+import org.junit.BeforeClass;
+
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.apache.cassandra.config.CassandraRelevantProperties.UDF_SECURITY_MECHANISM;
+
+/** Tests the settings that permit restricted System method calls with an installed security manager. */
+public class UFInsecureSecurityManagerTest extends UFInsecureSystemAccessTest
+{
+ @BeforeClass
+ public static void setUpClass()
+ {
+ Assume.assumeTrue(Runtime.version().feature() < 24);
+ UDF_SECURITY_MECHANISM.setString("securitymanager");
+ CQLTester.setUpClass();
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFInsecureSystemAccessTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFInsecureSystemAccessTest.java
new file mode 100644
index 000000000000..7c99f32b71d1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFInsecureSystemAccessTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.cassandra.cql3.validation.entities;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
+
+import static org.apache.cassandra.config.CassandraRelevantProperties.UDF_SECURITY_MECHANISM;
+import static org.junit.Assert.assertEquals;
+
+/** Tests both settings required for insecure system access. */
+public class UFInsecureSystemAccessTest extends CQLTester
+{
+ @BeforeClass
+ public static void setUpClass()
+ {
+ UDF_SECURITY_MECHANISM.setString("sandbox");
+ CQLTester.setUpClass();
+ }
+
+ @Test
+ public void systemAccessRequiresBothSettings() throws Throwable
+ {
+ assertEquals(ThreadAwareSecurityManager.useSecurityManager(), System.getSecurityManager() != null);
+ createTable("CREATE TABLE %s (key int PRIMARY KEY, val double)");
+ execute("INSERT INTO %s (key, val) VALUES (1, 0)");
+ Config conf = DatabaseDescriptor.getRawConfig();
+ boolean threads = conf.user_defined_functions_threads_enabled;
+ boolean insecure = conf.allow_extra_insecure_udfs;
+ String[] sources = {
+ "System.getProperty(\"java.version\"); return 0d;", // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ "System.getenv(\"PATH\"); return 0d;", // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ "Integer.getInteger(\"udf-test\"); return 0d;", // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ "Long.getLong(\"udf-test\"); return 0d;", // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ "Boolean.getBoolean(\"udf-test\"); return 0d;" // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ };
+ try
+ {
+ for (boolean useThreads : new boolean[]{ false, true })
+ {
+ for (boolean allowInsecure : new boolean[]{ false, true })
+ {
+ conf.user_defined_functions_threads_enabled = useThreads;
+ conf.allow_extra_insecure_udfs = allowInsecure;
+ for (String source : sources)
+ {
+ if (!useThreads && allowInsecure)
+ {
+ String name = createFunction(KEYSPACE_PER_TEST, "double", function("%s", source));
+ assertRows(execute("SELECT " + name + "(val) FROM %s WHERE key=1"), row(0d));
+ }
+ else if (useThreads && ThreadAwareSecurityManager.useSecurityManager())
+ {
+ String name = createFunction(KEYSPACE_PER_TEST, "double", function("%s", source));
+ assertInvalidMessage("access denied", "SELECT " + name + "(val) FROM %s WHERE key=1");
+ }
+ else
+ {
+ assertInvalid(function(KEYSPACE + ".restricted", source));
+ }
+ }
+ assertInvalidMessage("call to java.lang.ClassLoader.getPlatformClassLoader()",
+ function(KEYSPACE + ".restricted", "ClassLoader.getPlatformClassLoader(); return 0d;"));
+ assertInvalid(function(KEYSPACE + ".restricted", "Runtime.getRuntime(); return 0d;"));
+ }
+ }
+ }
+ finally
+ {
+ conf.user_defined_functions_threads_enabled = threads;
+ conf.allow_extra_insecure_udfs = insecure;
+ }
+ }
+
+ private static String function(String name, String source)
+ {
+ return "CREATE OR REPLACE FUNCTION " + name + "(val double) RETURNS NULL ON NULL INPUT " +
+ "RETURNS double LANGUAGE JAVA AS '" + source + "';";
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFSandboxTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFSandboxTest.java
new file mode 100644
index 000000000000..de7ef9fa3958
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFSandboxTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.cassandra.cql3.validation.entities;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.apache.cassandra.config.CassandraRelevantProperties.UDF_SECURITY_MECHANISM;
+import static org.junit.Assert.assertNull;
+
+/** Runs the security and timeout tests without an installed security manager. */
+public class UFSandboxTest extends UFSecurityTest
+{
+ @BeforeClass
+ public static void setUpClass()
+ {
+ UDF_SECURITY_MECHANISM.setString("sandbox");
+ CQLTester.setUpClass();
+ }
+
+ @Test
+ public void noSecurityManager()
+ {
+ assertNull(System.getSecurityManager());
+ }
+
+ @Test
+ public void fileFormatterIsRejected() throws Throwable
+ {
+ assertInvalid("CREATE FUNCTION " + KEYSPACE + ".invalid_formatter(val double) " +
+ "RETURNS NULL ON NULL INPUT RETURNS double LANGUAGE JAVA " +
+ "AS 'try { new java.util.Formatter(\"udf-sandbox-test\"); } catch (Exception e) {} return 0d;'");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityManagerTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityManagerTest.java
new file mode 100644
index 000000000000..df8eccbacade
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityManagerTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.cassandra.cql3.validation.entities;
+
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.apache.cassandra.config.CassandraRelevantProperties.UDF_SECURITY_MECHANISM;
+import static org.junit.Assert.assertNotNull;
+
+/** Runs the security and timeout tests with the explicit security manager setting. */
+public class UFSecurityManagerTest extends UFSecurityTest
+{
+ @BeforeClass
+ public static void setUpClass()
+ {
+ Assume.assumeTrue(Runtime.version().feature() < 24);
+ UDF_SECURITY_MECHANISM.setString("securitymanager");
+ CQLTester.setUpClass();
+ }
+
+ @Test
+ public void securityManagerInstalled()
+ {
+ assertNotNull(System.getSecurityManager());
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityTest.java
index 7716e8b4e5ae..02dd0477b2ae 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UFSecurityTest.java
@@ -18,16 +18,27 @@
package org.apache.cassandra.cql3.validation.entities;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
import java.security.AccessControlException;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Test;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.exceptions.FunctionExecutionException;
+import org.apache.cassandra.security.ThreadAwareSecurityManager;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.utils.JavaDriverUtils;
@@ -39,22 +50,36 @@ public void testSecurityPermissions() throws Throwable
createTable("CREATE TABLE %s (key int primary key, dval double)");
execute("INSERT INTO %s (key, dval) VALUES (?, ?)", 1, 1d);
- // Java UDFs
+ // Java user-defined functions (UDFs)
- try
+ // UDFs must load System for timing and array operations. A SecurityManager blocks getProperty at runtime.
+ // The bytecode sandbox blocks it when Cassandra creates the function.
+ if (ThreadAwareSecurityManager.useSecurityManager())
{
- String fName = createFunction(KEYSPACE_PER_TEST, "double",
- "CREATE OR REPLACE FUNCTION %s(val double) " +
- "RETURNS NULL ON NULL INPUT " +
- "RETURNS double " +
- "LANGUAGE JAVA\n" +
- "AS 'System.getProperty(\"foo.bar.baz\"); return 0d;';"); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
- execute("SELECT " + fName + "(dval) FROM %s WHERE key=1");
- Assert.fail();
+ try
+ {
+ String fName = createFunction(KEYSPACE_PER_TEST, "double",
+ "CREATE OR REPLACE FUNCTION %s(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS 'System.getProperty(\"foo.bar.baz\"); return 0d;';"); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ execute("SELECT " + fName + "(dval) FROM %s WHERE key=1");
+ Assert.fail();
+ }
+ catch (FunctionExecutionException e)
+ {
+ assertAccessControlException("System.getProperty(\"foo.bar.baz\"); return 0d;", e); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ }
}
- catch (FunctionExecutionException e)
+ else
{
- assertAccessControlException("System.getProperty(\"foo.bar.baz\"); return 0d;", e); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ assertInvalidMessage("Java UDF validation failed: [call to java.lang.System.getProperty()]", // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_get_property(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS 'System.getProperty(\"foo.bar.baz\"); return 0d;';"); // checkstyle: suppress nearby 'blockSystemPropertyUsage'
}
String[] cfnSources =
@@ -124,7 +149,12 @@ public void testSecurityPermissions() throws Throwable
" org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize(0L); return 0d;" +
"} catch (Exception t) {" +
" throw new RuntimeException(t);" +
- '}'}
+ '}'},
+ // These classes match allowed package prefixes and expose restricted operations.
+ {"java.lang.ProcessHandle", "java.lang.ProcessHandle.current(); return 0d;"},
+ {"java.lang.StackWalker", "java.lang.StackWalker.getInstance(); return 0d;"},
+ {"java.lang.foreign.Linker","java.lang.foreign.Linker.nativeLinker(); return 0d;"},
+ {"java.lang.classfile.ClassFile","java.lang.classfile.ClassFile.of(); return 0d;"} // Java Development Kit (JDK) 24 and later.
};
for (String[] typeAndSource : typesAndSources)
@@ -136,6 +166,104 @@ public void testSecurityPermissions() throws Throwable
"LANGUAGE JAVA\n" +
"AS '" + typeAndSource[1] + "';");
}
+
+ // The class loader must resolve Module because Class references it.
+ // Test the verifier rules that block Module and ModuleLayer calls.
+ String[][] moduleApiSources =
+ {
+ {"java.lang.Class.getModule", "java.lang.Integer.class.getModule(); return 0d;"},
+ {"java.lang.ModuleLayer.boot", "java.lang.ModuleLayer.boot(); return 0d;"}
+ };
+ for (String[] moduleApiSource : moduleApiSources)
+ assertInvalidMessage("Java UDF validation failed: [call to " + moduleApiSource[0] + "()]",
+ "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_module_access(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS '" + moduleApiSource[1] + "';");
+ }
+
+ /**
+ * Tests the ClassLoader rule in the verifier.
+ * The verifier rejects all ClassLoader calls for both sandbox mechanisms.
+ */
+ @Test
+ public void testClassLoaderAccessRejected() throws Throwable
+ {
+ // These calls compile, then the verifier rejects them.
+ assertInvalidMessage("Java UDF validation failed: [call to java.lang.ClassLoader.getPlatformClassLoader()]",
+ createClassLoaderFunction("java.lang.ClassLoader.getPlatformClassLoader(); return 0d;"));
+ assertInvalidMessage("Java UDF validation failed: [call to java.lang.ClassLoader.getParent()]",
+ createClassLoaderFunction("((java.lang.ClassLoader) null).getParent(); return 0d;"));
+
+ // ClassLoader.resources returns a type that the UDF class loader cannot resolve.
+ // The compiler rejects this call.
+ assertInvalid(createClassLoaderFunction("((java.lang.ClassLoader) null).resources(\"x\"); return 0d;"));
+ }
+
+ private static String createClassLoaderFunction(String body)
+ {
+ return "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_classloader_access(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS '" + body + "';";
+ }
+
+ /**
+ * Checks each public ClassLoader method whose return type appears in loaderYielding.
+ * Confirms that a UDF cannot call these methods.
+ */
+ @Test
+ public void testClassLoaderLoaderYieldingMethodsDenied() throws Throwable
+ {
+ Set> loaderYielding = new HashSet<>(Arrays.asList(
+ ClassLoader.class, Class.class, URL.class, InputStream.class, Stream.class, Enumeration.class));
+
+ int checked = 0;
+ for (Method m : ClassLoader.class.getMethods())
+ {
+ if (m.getDeclaringClass() != ClassLoader.class || !Modifier.isPublic(m.getModifiers()))
+ continue;
+ if (!loaderYielding.contains(m.getReturnType()))
+ continue;
+
+ String target = Modifier.isStatic(m.getModifiers())
+ ? "java.lang.ClassLoader"
+ : "((java.lang.ClassLoader) null)";
+ StringBuilder args = new StringBuilder();
+ for (Class> p : m.getParameterTypes())
+ {
+ if (args.length() > 0)
+ args.append(", ");
+ args.append(defaultArg(p));
+ }
+ // Handle declared exceptions so the test body can compile.
+ String body = "try { " + target + '.' + m.getName() + '(' + args + "); } catch (Throwable __t) {} return 0d;";
+
+ // Resolvable calls reach the verifier. Calls with blocked return types fail during compilation.
+ assertInvalid("CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_classloader_enum(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS '" + body + "';");
+ checked++;
+ }
+ // Require enough methods to show that reflection exercised the check.
+ Assert.assertTrue("Expected several loader/resource-yielding ClassLoader methods, found " + checked, checked >= 3);
+ }
+
+ private static String defaultArg(Class> type)
+ {
+ if (type == String.class) return "\"x\"";
+ if (type == boolean.class) return "false";
+ if (type == char.class) return "'a'";
+ if (type == byte.class || type == short.class || type == int.class) return "0";
+ if (type == long.class) return "0L";
+ if (type == float.class) return "0f";
+ if (type == double.class) return "0d";
+ // Use a typed null for object and array parameters.
+ return "(" + type.getCanonicalName() + ") null";
}
private static void assertAccessControlException(String script, FunctionExecutionException e)
@@ -146,6 +274,86 @@ private static void assertAccessControlException(String script, FunctionExecutio
Assert.fail("no AccessControlException for " + script + " (got " + e + ')');
}
+ /**
+ * Confirms that the bytecode sandbox rejects restricted System methods when Cassandra creates a function.
+ * It also confirms that timing and array methods remain available.
+ */
+ @Test
+ public void testSandboxBlocksDangerousSystemMethods() throws Throwable
+ {
+ Assume.assumeFalse("legacy SecurityManager mechanism in use; covered by testSecurityPermissions",
+ ThreadAwareSecurityManager.useSecurityManager());
+
+ String[][] methodAndSource =
+ {
+ {"exit", "System.exit(1); return 0d;"},
+ {"setProperty", "System.setProperty(\"foo\", \"bar\"); return 0d;"}, // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ {"getProperty", "System.getProperty(\"foo\"); return 0d;"}, // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ {"getenv", "System.getenv(\"PATH\"); return 0d;"}, // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ {"loadLibrary", "System.loadLibrary(\"foo\"); return 0d;"},
+ {"setSecurityManager", "System.setSecurityManager(null); return 0d;"}
+ };
+
+ for (String[] ms : methodAndSource)
+ assertInvalidMessage("Java UDF validation failed: [call to java.lang.System." + ms[0] + "()]",
+ "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_system_" + ms[0].toLowerCase() + "(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS '" + ms[1] + "';");
+
+ // Test property aliases and the system logger factory.
+ int alias = 0;
+ String[][] aliasSources =
+ {
+ {"java.util.Locale.setDefault", "java.util.Locale.setDefault(java.util.Locale.FRANCE); return 0d;"},
+ {"java.util.Locale.setDefault", "java.util.Locale.setDefault(java.util.Locale.Category.DISPLAY, java.util.Locale.FRANCE); return 0d;"},
+ {"java.util.TimeZone.setDefault", "java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(\"UTC\")); return 0d;"},
+ {"java.util.SimpleTimeZone.setDefault", "java.util.SimpleTimeZone.setDefault(java.util.TimeZone.getTimeZone(\"UTC\")); return 0d;"},
+ {"java.lang.System.getLogger", "System.getLogger(\"x\"); return 0d;"},
+ {"java.lang.System$LoggerFinder.getLoggerFinder", "System.LoggerFinder.getLoggerFinder(); return 0d;"},
+ {"java.lang.Integer.getInteger", "Integer.getInteger(\"x\"); return 0d;"}, // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ {"java.lang.Long.getLong", "Long.getLong(\"x\"); return 0d;"}, // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ {"java.lang.Boolean.getBoolean", "Boolean.getBoolean(\"x\"); return 0d;"} // checkstyle: suppress nearby 'blockSystemPropertyUsage'
+ };
+ for (String[] as : aliasSources)
+ assertInvalidMessage("Java UDF validation failed: [call to " + as[0] + "()]",
+ "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_alias_" + (alias++) + "(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS '" + as[1] + "';");
+
+ // Timing methods remain available to UDFs.
+ String fName = createFunction(KEYSPACE_PER_TEST, "double",
+ "CREATE OR REPLACE FUNCTION %s(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS 'return (double) (System.nanoTime() - System.currentTimeMillis());';");
+ createTable("CREATE TABLE %s (key int PRIMARY KEY, val double)");
+ execute("INSERT INTO %s (key, val) VALUES (1, 0)");
+ Assert.assertEquals(1, execute("SELECT " + fName + "(val) FROM %s WHERE key=1").size());
+ String arrayFunction = createFunction(KEYSPACE_PER_TEST, "double",
+ "CREATE FUNCTION %s(val double) RETURNS NULL ON NULL INPUT " +
+ "RETURNS double LANGUAGE JAVA AS 'double[] a = { val }; " +
+ "double[] b = new double[1]; System.arraycopy(a, 0, b, 0, 1); return b[0];'");
+ assertRows(execute("SELECT " + arrayFunction + "(val) FROM %s WHERE key=1"), row(0d));
+ }
+
+ /** Confirms that a UDF cannot emit a second class file that the verifier would not inspect. */
+ @Test
+ public void testRejectsAdditionalClasses() throws Throwable
+ {
+ // An anonymous Object subclass emits a second class file without using a blocked method.
+ assertInvalidMessage("the function must not declare additional classes",
+ "CREATE OR REPLACE FUNCTION " + KEYSPACE + ".invalid_inner_class(val double) " +
+ "RETURNS NULL ON NULL INPUT " +
+ "RETURNS double " +
+ "LANGUAGE JAVA\n" +
+ "AS 'return (double) new Object(){}.hashCode();';");
+ }
+
@Test
public void testAmokUDF() throws Throwable
{
diff --git a/test/unit/org/apache/cassandra/security/JMXSubjectsTest.java b/test/unit/org/apache/cassandra/security/JMXSubjectsTest.java
new file mode 100644
index 000000000000..8dce187f9657
--- /dev/null
+++ b/test/unit/org/apache/cassandra/security/JMXSubjectsTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.cassandra.security;
+
+import java.security.PrivilegedAction;
+import java.util.concurrent.Callable;
+
+import javax.security.auth.Subject;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+public class JMXSubjectsTest
+{
+ @Test
+ public void authenticatedSubject() throws Exception
+ {
+ assertNull(JMXSubjects.current());
+ Subject subject = new Subject();
+ Subject other = new Subject();
+ Subject.doAs(subject, (PrivilegedAction) () -> {
+ assertSame(subject, JMXSubjects.current());
+ Subject.doAs(other, (PrivilegedAction) () -> {
+ assertSame(other, JMXSubjects.current());
+ return null;
+ });
+ assertSame(subject, JMXSubjects.current());
+ return null;
+ });
+ assertNull(JMXSubjects.current());
+ }
+
+ @Test
+ public void reflectiveLookup() throws Exception
+ {
+ if (Runtime.version().feature() < 18)
+ return;
+ Subject subject = new Subject();
+ // Exercise the newer lookup on supported runtimes that already provide Subject.callAs.
+ Subject.class.getMethod("callAs", Subject.class, Callable.class).invoke(null, subject, (Callable) () -> {
+ assertSame(subject, JMXSubjects.currentSubject());
+ return null;
+ });
+ assertNull(JMXSubjects.currentSubject());
+ }
+}
diff --git a/test/unit/org/apache/cassandra/security/ThreadAwareSecurityManagerTest.java b/test/unit/org/apache/cassandra/security/ThreadAwareSecurityManagerTest.java
new file mode 100644
index 000000000000..e09ddda26cfe
--- /dev/null
+++ b/test/unit/org/apache/cassandra/security/ThreadAwareSecurityManagerTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.cassandra.security;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.security.Policy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import com.google.common.base.StandardSystemProperty;
+
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.utils.logging.SlowQueriesAppender;
+import org.apache.cassandra.utils.logging.VirtualTableAppender;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ThreadAwareSecurityManagerTest
+{
+ @Test
+ public void selection()
+ {
+ for (int version : new int[]{ 11, 17, 21, 23, 24, 25 })
+ {
+ assertEquals(version < 24, ThreadAwareSecurityManager.useSecurityManager("auto", version));
+ assertTrue(ThreadAwareSecurityManager.useSecurityManager("securitymanager", version));
+ assertFalse(ThreadAwareSecurityManager.useSecurityManager("sandbox", version));
+ }
+ assertFalse(ThreadAwareSecurityManager.useSecurityManager(" SANDBOX ", 11));
+ for (String invalid : new String[]{ "", "false", "disabled", "security-manager" })
+ {
+ try
+ {
+ ThreadAwareSecurityManager.useSecurityManager(invalid, 11);
+ fail("Accepted " + invalid);
+ }
+ catch (ConfigurationException e)
+ {
+ assertTrue(e.getMessage().contains("cassandra.udf.security_mechanism"));
+ }
+ }
+ }
+
+ @Test
+ public void startup() throws Exception
+ {
+ startup("sandbox", false, false);
+ startup("auto", Runtime.version().feature() < 24, false);
+ startup("securitymanager", true, Runtime.version().feature() >= 24);
+ startup("invalid", false, true);
+ if (Runtime.version().feature() >= 12)
+ startup("securitymanager", true, true, "-Djava.security.manager=disallow");
+ }
+
+ private static void startup(String mechanism, boolean installed, boolean failure, String... options) throws Exception
+ {
+ startup(mechanism, Startup.class, Arrays.asList(Boolean.toString(installed), Boolean.toString(failure)), options);
+ }
+
+ @Test
+ public void loggingStartup() throws Exception
+ {
+ for (String mechanism : new String[]{ "sandbox", "auto" })
+ {
+ startup(mechanism, LoggingStartup.class, Arrays.asList("virtual"));
+ startup(mechanism, LoggingStartup.class, Arrays.asList("slow"));
+ }
+ }
+
+ private static void startup(String mechanism, Class> mainClass, List args, String... options) throws Exception
+ {
+ List command = new ArrayList<>();
+ command.add(StandardSystemProperty.JAVA_HOME.value() + File.separator + "bin" + File.separator + "java");
+ if (Runtime.version().feature() >= 17 && Runtime.version().feature() < 24)
+ command.add("-Djava.security.manager=allow");
+ command.addAll(Arrays.asList(options));
+ command.add("-Dcassandra.udf.security_mechanism=" + mechanism);
+ command.add("-cp");
+ command.add(StandardSystemProperty.JAVA_CLASS_PATH.value());
+ command.add(mainClass.getName());
+ command.addAll(args);
+ Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
+ try
+ {
+ assertTrue("Startup did not finish", process.waitFor(30, TimeUnit.SECONDS));
+ String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ assertEquals(output, 0, process.exitValue());
+ }
+ finally
+ {
+ process.destroyForcibly();
+ }
+ }
+
+ public static class LoggingStartup
+ {
+ @SuppressWarnings("unchecked")
+ public static void main(String[] args)
+ {
+ DatabaseDescriptor.clientInitialization();
+ Supplier> factory = args[0].equals("virtual") ? VirtualTableAppender::new : SlowQueriesAppender::new;
+ Appender> first = factory.get();
+ Appender> second = factory.get();
+ first.setName("first");
+ second.setName("second");
+ Logger logger = (Logger) LoggerFactory.getLogger(LoggingStartup.class);
+ // Logback supplies LoggingEvent instances to these appenders.
+ logger.addAppender((Appender) first);
+ logger.addAppender((Appender) second);
+ try
+ {
+ ThreadAwareSecurityManager.install();
+ throw new AssertionError("Expected duplicate appender failure");
+ }
+ catch (IllegalStateException e)
+ {
+ if (!e.getMessage().contains("multiple appenders of class " + first.getClass().getName()))
+ throw e;
+ }
+ }
+ }
+
+ public static class Startup
+ {
+ public static void main(String[] args)
+ {
+ DatabaseDescriptor.clientInitialization();
+ Policy policy = Runtime.version().feature() < 24 ? Policy.getPolicy() : null;
+ ThreadAwareSecurityManager.isSecuredThread();
+ boolean failure = Boolean.parseBoolean(args[1]);
+ try
+ {
+ ThreadAwareSecurityManager.install();
+ if (failure)
+ throw new AssertionError("Expected configuration failure");
+ if ((System.getSecurityManager() != null) != Boolean.parseBoolean(args[0]))
+ throw new AssertionError("Unexpected installed security manager");
+ if (!Boolean.parseBoolean(args[0]) && Runtime.version().feature() < 24 && policy != Policy.getPolicy())
+ throw new AssertionError("Sandbox changed the security policy");
+ }
+ catch (ConfigurationException e)
+ {
+ if (!failure)
+ throw e;
+ }
+ }
+ }
+}