Support audit logging for JMX operations

Patch by Abe Ratnofsky; reviewed by Bernardo Botella, Doug Rohrer, Francisco Guerrero for CASSANDRA-20128
This commit is contained in:
Abe Ratnofsky 2024-12-06 13:41:31 -05:00 committed by Francisco Guerrero
parent 5dcdd0f048
commit c853efffa8
12 changed files with 411 additions and 30 deletions

View File

@ -1,4 +1,5 @@
5.1
* Support audit logging for JMX operations (CASSANDRA-20128)
* Enable sorting of nodetool status output (CASSANDRA-20104)
* Support downgrading after CMS is initialized (CASSANDRA-20145)
* Deprecate IEndpointSnitch (CASSANDRA-19488)

View File

@ -338,7 +338,7 @@
<!-- needed to compile org.apache.cassandra.utils.JMXServerUtils -->
<!-- needed to compile org.apache.cassandra.distributed.impl.Instance-->
<!-- needed to compile org.apache.cassandra.utils.memory.BufferPool -->
<property name="jdk11plus-javac-exports" value="--add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.transport.tcp=ALL-UNNAMED --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED" />
<property name="jdk11plus-javac-exports" value="--add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.transport.tcp=ALL-UNNAMED --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-exports java.management/com.sun.jmx.remote.security=ALL-UNNAMED" />
<!--
Add all the dependencies.

View File

@ -23,5 +23,5 @@ package org.apache.cassandra.audit;
*/
public enum AuditLogEntryCategory
{
QUERY, DML, DDL, DCL, OTHER, AUTH, ERROR, PREPARE
QUERY, DML, DDL, DCL, OTHER, AUTH, ERROR, PREPARE, JMX
}

View File

@ -71,7 +71,8 @@ public enum AuditLogEntryType
LOGIN_ERROR(AuditLogEntryCategory.AUTH),
UNAUTHORIZED_ATTEMPT(AuditLogEntryCategory.AUTH),
LOGIN_SUCCESS(AuditLogEntryCategory.AUTH),
LIST_SUPERUSERS(AuditLogEntryCategory.DCL);
LIST_SUPERUSERS(AuditLogEntryCategory.DCL),
JMX(AuditLogEntryCategory.JMX);
private final AuditLogEntryCategory category;

View File

@ -127,7 +127,21 @@ final class AuditLogFilter
}
/**
* Checks whether a give AuditLog Entry is filtered or not
* Checks whether a given AuditLogEntryCategory is filtered or not.
*
* This is useful when creating an audit log entry might be expensive, and checking the category before formatting
* is less costly.
*
* @param category AuditLogEntryCategory to verify
* @return true if it is filtered, false otherwise
*/
boolean isFiltered(AuditLogEntryCategory category)
{
return isFiltered(category.toString(), includedCategories, excludedCategories);
}
/**
* Checks whether a given AuditLog Entry is filtered or not
*
* @param auditLogEntry AuditLogEntry to verify
* @return true if it is filtered, false otherwise

View File

@ -18,13 +18,26 @@
package org.apache.cassandra.audit;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.management.MBeanServer;
import javax.management.openmbean.CompositeData;
import javax.management.remote.MBeanServerForwarder;
import javax.security.auth.Subject;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@ -49,15 +62,18 @@ import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.Message;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.JmxInvocationListener;
import org.apache.cassandra.utils.MBeanWrapper;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
import static org.apache.cassandra.audit.AuditLogEntryType.JMX;
/**
* Central location for managing the logging of client/user-initated actions (like queries, log in commands, and so on).
*
*/
public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listener, AuditLogManagerMBean
public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listener, AuditLogManagerMBean, JmxInvocationListener
{
private static final Logger logger = LoggerFactory.getLogger(AuditLogManager.class);
@ -69,6 +85,9 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
private volatile AuditLogFilter filter;
private volatile AuditLogOptions auditLogOptions;
// Only reset in tests
private MBeanServerForwarder mbsf = createMBeanServerForwarder();
private AuditLogManager()
{
auditLogOptions = DatabaseDescriptor.getAuditLoggingOptions();
@ -159,7 +178,7 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
{
builder.setType(AuditLogEntryType.LOGIN_ERROR);
}
else
else if (logEntry.getType() != JMX)
{
builder.setType(AuditLogEntryType.REQUEST_FAILURE);
}
@ -400,4 +419,112 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
return PasswordObfuscator.obfuscate(e.getMessage());
}
private static class JmxFormatter
{
private static String user(Subject subject)
{
if (subject == null)
return "null";
StringJoiner joiner = new StringJoiner(", ");
for (Principal principal : subject.getPrincipals())
joiner.add(Objects.toString(principal));
return joiner.toString();
}
private static String method(Method method, Object[] args)
{
String argsFmt = "";
if (args != null)
{
StringJoiner joiner = new StringJoiner(", ");
for (Object arg : args)
joiner.add(Objects.toString(arg));
argsFmt = joiner.toString();
}
return String.format("%s#%s(%s)", method.getDeclaringClass().getCanonicalName(), method.getName(), argsFmt);
}
}
@Override
public void onInvocation(Subject subject, Method method, Object[] args)
{
if (filter.isFiltered(AuditLogEntryCategory.JMX))
return;
AuditLogEntry entry = new AuditLogEntry.Builder(JMX)
.setOperation(String.format("JMX INVOCATION: %s", JmxFormatter.method(method, args)))
.setUser(JmxFormatter.user(subject))
.build();
log(entry);
}
@Override
public void onFailure(Subject subject, Method method, Object[] args, Exception exception)
{
if (filter.isFiltered(AuditLogEntryCategory.JMX))
return;
AuditLogEntry entry = new AuditLogEntry.Builder(JMX)
.setOperation(String.format("JMX FAILURE: %s due to %s", JmxFormatter.method(method, args), exception.getClass().getSimpleName()))
.setUser(JmxFormatter.user(subject))
.build();
log(entry, exception);
}
private class JmxHandler implements InvocationHandler
{
private MBeanServer mbs = null;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
// See AuthorizationProxy.invoke
if ("setMBeanServer".equals(method.getName()))
{
if (args[0] == null)
throw new IllegalArgumentException("Null MBeanServer");
if (mbs != null)
throw new IllegalArgumentException("MBeanServer already initialized");
mbs = (MBeanServer) args[0];
return null;
}
AccessControlContext acc = AccessController.getContext();
Subject subject = Subject.getSubject(acc);
try
{
Object invoke = method.invoke(mbs, args);
AuditLogManager.this.onInvocation(subject, method, args);
return invoke;
}
catch (InvocationTargetException e)
{
AuditLogManager.instance.onFailure(subject, method, args, e);
throw e.getCause();
}
}
}
private MBeanServerForwarder createMBeanServerForwarder()
{
InvocationHandler handler = new JmxHandler();
Class<?>[] interfaces = { MBeanServerForwarder.class };
Object proxy = Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, handler);
return (MBeanServerForwarder) proxy;
}
@VisibleForTesting
void resetMBeanServerForwarder()
{
this.mbsf = createMBeanServerForwarder();
}
public MBeanServerForwarder getMBeanServerForwarder()
{
return mbsf;
}
}

View File

@ -84,7 +84,7 @@ public class CassandraPrincipal implements Principal, Serializable
@Override
public String toString()
{
return ("CassandraPrincipal: " + name);
return ("CassandraPrincipal: " + name);
}
/**

View File

@ -39,9 +39,11 @@ import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.auth.*;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.JmxInvocationListener;
import org.apache.cassandra.utils.MBeanWrapper;
/**
@ -140,43 +142,57 @@ public class AuthorizationProxy implements InvocationHandler
*/
protected BooleanSupplier isAuthSetupComplete = () -> StorageService.instance.isAuthSetupComplete();
protected JmxInvocationListener listener = AuditLogManager.instance;
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
String methodName = method.getName();
if ("getMBeanServer".equals(methodName))
throw new SecurityException("Access denied");
// Corresponds to MBeanServer.invoke
if (methodName.equals("invoke") && args.length == 4)
checkVulnerableMethods(args);
// Retrieve Subject from current AccessControlContext
AccessControlContext acc = AccessController.getContext();
Subject subject = Subject.getSubject(acc);
// Allow setMBeanServer iff performed on behalf of the connector server itself
if (("setMBeanServer").equals(methodName))
try
{
if (subject != null)
if ("getMBeanServer".equals(methodName))
throw new SecurityException("Access denied");
if (args[0] == null)
throw new IllegalArgumentException("Null MBeanServer");
// Corresponds to MBeanServer.invoke
if (methodName.equals("invoke") && args.length == 4)
checkVulnerableMethods(args);
if (mbs != null)
throw new IllegalArgumentException("MBeanServer already initialized");
// Allow setMBeanServer iff performed on behalf of the connector server itself
if (("setMBeanServer").equals(methodName))
{
if (subject != null)
throw new SecurityException("Access denied");
mbs = (MBeanServer) args[0];
return null;
if (args[0] == null)
throw new IllegalArgumentException("Null MBeanServer");
if (mbs != null)
throw new IllegalArgumentException("MBeanServer already initialized");
mbs = (MBeanServer) args[0];
return null;
}
if (authorize(subject, methodName, args))
{
Object invoke = invoke(method, args);
listener.onInvocation(subject, method, args);
return invoke;
}
throw new SecurityException("Access Denied");
}
catch (Exception e)
{
listener.onFailure(subject, method, args, e);
throw e;
}
if (authorize(subject, methodName, args))
return invoke(method, args);
throw new SecurityException("Access Denied");
}
/**

View File

@ -53,6 +53,7 @@ import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.auth.jmx.AuthenticationProxy;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.JMXServerOptions;
@ -135,6 +136,9 @@ public class JMXServerUtils
// If a custom authz proxy was created, attach it to the server now.
if (authzProxy != null)
jmxServer.setMBeanServerForwarder(authzProxy);
else
jmxServer.setMBeanServerForwarder(AuditLogManager.instance.getMBeanServerForwarder());
jmxServer.start();
registry.setRemoteServerStub(server.toStub());
@ -274,7 +278,7 @@ public class JMXServerUtils
}
return String.format(urlTemplate, hostName, port);
}
private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator
{
private static final MethodHandle ctorHandle;

View File

@ -0,0 +1,50 @@
/*
* 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.utils;
import java.lang.reflect.Method;
import javax.security.auth.Subject;
/**
* Listener for operations executed over JMX.
*/
public interface JmxInvocationListener
{
/**
* Listener called when an attempted invocation is successful.
*
* @param subject The subject attempting invocation, depends on how JMX authentication is configured
* @param method Invoked method
* @param args Invoked method arguments
*/
default void onInvocation(Subject subject, Method method, Object[] args)
{}
/**
* Listener called when an attempted invocation throws an exception. This could happen before or after the
* underlying method is invoked, due to invocation wrappers such as {@link org.apache.cassandra.auth.jmx.AuthorizationProxy#invoke(Object, Method, Object[])}.
*
* @param subject The subject attempting invocation, depends on how JMX authentication is configured
* @param method Invoked method
* @param args Invoked method arguments
* @param cause Exception thrown by the attempted invocation
*/
default void onFailure(Subject subject, Method method, Object[] args, Exception cause)
{}
}

View File

@ -17,12 +17,24 @@
*/
package org.apache.cassandra.audit;
import org.junit.After;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.server.RMISocketFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
@ -36,15 +48,35 @@ import com.datastax.driver.core.exceptions.NoHostAvailableException;
import com.datastax.driver.core.exceptions.SyntaxError;
import net.openhft.chronicle.queue.RollCycles;
import org.apache.cassandra.auth.AuthEvents;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.JMXResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.auth.RoleResource;
import org.apache.cassandra.auth.StubAuthorizer;
import org.apache.cassandra.auth.jmx.AbstractJMXAuthTest;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.JMXServerOptions;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryEvents;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.JMXServerUtils;
import org.assertj.core.api.Assertions;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_AUTHORIZER;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_LOCAL_PORT;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_REMOTE_LOGIN_CONFIG;
import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_AUTHENTICATE;
import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_SECURITY_AUTH_LOGIN_CONFIG;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.emptyCollectionOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
@ -758,6 +790,108 @@ public class AuditLoggerTest extends CQLTester
assertEquals("/xyz/not/null", AuditLogManager.instance.getAuditLogOptions().archive_command);
}
@Test
public void testJMXAuditLogs() throws Throwable
{
// Need to use distinct ports, otherwise would get RMI registry object ID collision, even with server shutdown between
testJMXAuditLogs(false, getAutomaticallyAllocatedPort(InetAddress.getLoopbackAddress()));
testJMXAuditLogs(true, getAutomaticallyAllocatedPort(InetAddress.getLoopbackAddress()));
}
private void testJMXAuditLogs(boolean enableAuthorizationProxy, int port) throws Throwable
{
if (enableAuthorizationProxy)
{
// Set up JMX; see AbstractJMXAuthTest.setupAuthorizer
IAuthorizer authorizer = new StubAuthorizer();
Field authorizerField = DatabaseDescriptor.class.getDeclaredField("authorizer");
authorizerField.setAccessible(true);
authorizerField.set(null, authorizer);
DatabaseDescriptor.setPermissionsValidity(0);
}
JMXResource tableMBean = JMXResource.mbean("org.apache.cassandra.db:type=Tables,keyspace=system_auth,table=roles");
if (enableAuthorizationProxy)
{
DatabaseDescriptor.getAuthorizer().grant(AuthenticatedUser.SYSTEM_USER,
Permission.ALL,
tableMBean,
RoleResource.role("test_role"));
}
DatabaseDescriptor.setAuditLoggingOptions(new AuditLogOptions.Builder()
.withEnabled(true)
.withBlock(true)
.withLogger("InMemoryAuditLogger", null)
.build());
if (enableAuthorizationProxy)
{
String config = Paths.get(ClassLoader.getSystemResource("auth/cassandra-test-jaas.conf").toURI()).toString();
COM_SUN_MANAGEMENT_JMXREMOTE_AUTHENTICATE.setBoolean(true);
JAVA_SECURITY_AUTH_LOGIN_CONFIG.setString(config);
CASSANDRA_JMX_REMOTE_LOGIN_CONFIG.setString("TestLogin");
CASSANDRA_JMX_AUTHORIZER.setString(AbstractJMXAuthTest.NoSuperUserAuthorizationProxy.class.getName());
}
CASSANDRA_JMX_LOCAL_PORT.setInt(port);
JMXServerOptions options = JMXServerOptions.createParsingSystemProperties();
options.jmx_encryption_options.applyConfig();
JMXServerUtils.createJMXServer(options, "localhost").start();
JMXServiceURL jmxUrl = new JMXServiceURL(String.format("service:jmx:rmi:///jndi/rmi://localhost:%d/jmxrmi", port));
Map<String, Object> env = new HashMap<>();
env.put("com.sun.jndi.rmi.factory.socket", RMISocketFactory.getDefaultSocketFactory());
JMXConnector jmxc = JMXConnectorFactory.connect(jmxUrl, env);
MBeanServerConnection connection = jmxc.getMBeanServerConnection();
// Setting up the connection will cause a few JMX methods to be called, so need to reset to empty
Assert.assertThat(((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue, not(emptyCollectionOf(AuditLogEntry.class)));
((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue.clear();
// Do an operation that doesn't fail
ColumnFamilyStoreMBean proxy = JMX.newMBeanProxy(connection,
ObjectName.getInstance(tableMBean.getObjectName()),
ColumnFamilyStoreMBean.class);
proxy.getTableName();
AuditLogEntry logEntry = ((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue.poll();
assertEquals(AuditLogEntryType.JMX, logEntry.getType());
assertThat(logEntry.getOperation(), containsString("JMX INVOCATION"));
if (enableAuthorizationProxy)
assertThat(logEntry.getUser(), is("CassandraPrincipal: test_role"));
else
assertThat(logEntry.getUser(), is("null"));
assertEquals(0, ((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue.size());
// Do an operation that fails
tableMBean = JMXResource.mbean("org.apache.cassandra.db:type=Tables,keyspace=system_auth,table=roles");
proxy = JMX.newMBeanProxy(connection,
ObjectName.getInstance(tableMBean.getObjectName()),
ColumnFamilyStoreMBean.class);
ColumnFamilyStoreMBean finalProxy = proxy;
Assertions.assertThatThrownBy(() -> finalProxy.setMinimumCompactionThreshold(Integer.MAX_VALUE))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("min_compaction_threshold cannot be larger than the max_compaction_threshold");
logEntry = ((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue.poll();
assertEquals(AuditLogEntryType.JMX, logEntry.getType());
assertThat(logEntry.getOperation(), stringContainsInOrder("JMX INVOCATION", "getClassLoaderFor"));
assertThat(logEntry.getUser(), containsString("null"));
// 2 JMX calls are produced, one to search for class, then one to invoke
logEntry = ((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue.poll();
assertEquals(AuditLogEntryType.JMX, logEntry.getType());
assertThat(logEntry.getOperation(), stringContainsInOrder("JMX FAILURE", "setAttribute"));
if (enableAuthorizationProxy)
assertThat(logEntry.getUser(), is("CassandraPrincipal: test_role"));
else
assertThat(logEntry.getUser(), is("null"));
Assertions.assertThat(((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).inMemQueue).isEmpty();
AuditLogManager.instance.resetMBeanServerForwarder();
}
/**
* Helper methods for Audit Log CQL Testing
*/

View File

@ -22,6 +22,7 @@ import java.lang.reflect.Field;
import java.rmi.server.RMISocketFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
@ -39,6 +40,11 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.apache.cassandra.audit.AuditLogEntry;
import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.audit.AuditLogOptions;
import org.apache.cassandra.audit.InMemoryAuditLogger;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.CassandraPrincipal;
import org.apache.cassandra.auth.IAuthorizer;
@ -51,6 +57,7 @@ import org.apache.cassandra.config.JMXServerOptions;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.utils.JMXServerUtils;
import org.assertj.core.api.Assertions;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@ -64,6 +71,7 @@ public abstract class AbstractJMXAuthTest extends CQLTester
private RoleResource role;
private String tableName;
private JMXResource tableMBean;
private Queue<AuditLogEntry> auditLogs;
@Before
public void setup() throws Throwable
@ -73,6 +81,8 @@ public abstract class AbstractJMXAuthTest extends CQLTester
tableName = createTable("CREATE TABLE %s (k int, v int, PRIMARY KEY (k))");
tableMBean = JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,table=%s",
KEYSPACE, tableName));
AuditLogManager.instance.enable(DatabaseDescriptor.getAuditLoggingOptions());
auditLogs = ((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).internalQueue();
}
@Test
@ -157,6 +167,12 @@ public abstract class AbstractJMXAuthTest extends CQLTester
protected static void setupJMXServer(JMXServerOptions jmxServerOptions) throws Exception
{
DatabaseDescriptor.setAuditLoggingOptions(new AuditLogOptions.Builder()
.withEnabled(true)
.withBlock(true)
.withLogger("InMemoryAuditLogger", null)
.build());
jmxServerOptions.jmx_encryption_options.applyConfig();
jmxServer = JMXServerUtils.createJMXServer(jmxServerOptions, "localhost");
jmxServer.start();
@ -210,6 +226,10 @@ public abstract class AbstractJMXAuthTest extends CQLTester
private void assertAuthorized(MBeanAction action)
{
action.execute();
AuditLogEntry entry = nextAuditEvent(auditLogs);
Assertions.assertThat(entry.getType()).isSameAs(AuditLogEntryType.JMX);
Assertions.assertThat(entry.getUser()).contains(role.getRoleName());
Assertions.assertThat(entry.getOperation()).contains("JMX INVOCATION");
}
private void assertUnauthorized(MBeanAction action)
@ -222,9 +242,23 @@ public abstract class AbstractJMXAuthTest extends CQLTester
catch (SecurityException e)
{
assertEquals("Access Denied", e.getLocalizedMessage());
AuditLogEntry entry = nextAuditEvent(auditLogs);
Assertions.assertThat(entry.getType()).isSameAs(AuditLogEntryType.JMX);
Assertions.assertThat(entry.getUser()).contains(role.getRoleName());
Assertions.assertThat(entry.getOperation()).contains("JMX FAILURE");
Assertions.assertThat(entry.getOperation()).contains(e.getLocalizedMessage());
}
}
private static AuditLogEntry nextAuditEvent(Queue<AuditLogEntry> auditLogs)
{
// When creating a new proxy, classloaders are loaded over JMX, so ignore those for permission assertions
AuditLogEntry next = auditLogs.remove();
if (next.getOperation().startsWith("JMX INVOCATION: javax.management.MBeanServer#getClassLoaderFor"))
return auditLogs.remove();
return next;
}
private void clearAllPermissions()
{
((StubAuthorizer) DatabaseDescriptor.getAuthorizer()).clear();