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 5.1
* Support audit logging for JMX operations (CASSANDRA-20128)
* Enable sorting of nodetool status output (CASSANDRA-20104) * Enable sorting of nodetool status output (CASSANDRA-20104)
* Support downgrading after CMS is initialized (CASSANDRA-20145) * Support downgrading after CMS is initialized (CASSANDRA-20145)
* Deprecate IEndpointSnitch (CASSANDRA-19488) * Deprecate IEndpointSnitch (CASSANDRA-19488)

View File

@ -338,7 +338,7 @@
<!-- needed to compile org.apache.cassandra.utils.JMXServerUtils --> <!-- needed to compile org.apache.cassandra.utils.JMXServerUtils -->
<!-- needed to compile org.apache.cassandra.distributed.impl.Instance--> <!-- needed to compile org.apache.cassandra.distributed.impl.Instance-->
<!-- needed to compile org.apache.cassandra.utils.memory.BufferPool --> <!-- 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. Add all the dependencies.

View File

@ -23,5 +23,5 @@ package org.apache.cassandra.audit;
*/ */
public enum AuditLogEntryCategory 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), LOGIN_ERROR(AuditLogEntryCategory.AUTH),
UNAUTHORIZED_ATTEMPT(AuditLogEntryCategory.AUTH), UNAUTHORIZED_ATTEMPT(AuditLogEntryCategory.AUTH),
LOGIN_SUCCESS(AuditLogEntryCategory.AUTH), LOGIN_SUCCESS(AuditLogEntryCategory.AUTH),
LIST_SUPERUSERS(AuditLogEntryCategory.DCL); LIST_SUPERUSERS(AuditLogEntryCategory.DCL),
JMX(AuditLogEntryCategory.JMX);
private final AuditLogEntryCategory category; 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 * @param auditLogEntry AuditLogEntry to verify
* @return true if it is filtered, false otherwise * @return true if it is filtered, false otherwise

View File

@ -18,13 +18,26 @@
package org.apache.cassandra.audit; 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.nio.ByteBuffer;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.UUID; import java.util.UUID;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.management.MBeanServer;
import javax.management.openmbean.CompositeData; 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.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList; 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.Message;
import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.JmxInvocationListener;
import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.MBeanWrapper;
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized; 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). * 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); 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 AuditLogFilter filter;
private volatile AuditLogOptions auditLogOptions; private volatile AuditLogOptions auditLogOptions;
// Only reset in tests
private MBeanServerForwarder mbsf = createMBeanServerForwarder();
private AuditLogManager() private AuditLogManager()
{ {
auditLogOptions = DatabaseDescriptor.getAuditLoggingOptions(); auditLogOptions = DatabaseDescriptor.getAuditLoggingOptions();
@ -159,7 +178,7 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
{ {
builder.setType(AuditLogEntryType.LOGIN_ERROR); builder.setType(AuditLogEntryType.LOGIN_ERROR);
} }
else else if (logEntry.getType() != JMX)
{ {
builder.setType(AuditLogEntryType.REQUEST_FAILURE); builder.setType(AuditLogEntryType.REQUEST_FAILURE);
} }
@ -400,4 +419,112 @@ public class AuditLogManager implements QueryEvents.Listener, AuthEvents.Listene
return PasswordObfuscator.obfuscate(e.getMessage()); 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 @Override
public String toString() 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.auth.*; import org.apache.cassandra.auth.*;
import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.JmxInvocationListener;
import org.apache.cassandra.utils.MBeanWrapper; import org.apache.cassandra.utils.MBeanWrapper;
/** /**
@ -140,43 +142,57 @@ public class AuthorizationProxy implements InvocationHandler
*/ */
protected BooleanSupplier isAuthSetupComplete = () -> StorageService.instance.isAuthSetupComplete(); protected BooleanSupplier isAuthSetupComplete = () -> StorageService.instance.isAuthSetupComplete();
protected JmxInvocationListener listener = AuditLogManager.instance;
@Override @Override
public Object invoke(Object proxy, Method method, Object[] args) public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable throws Throwable
{ {
String methodName = method.getName(); 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 // Retrieve Subject from current AccessControlContext
AccessControlContext acc = AccessController.getContext(); AccessControlContext acc = AccessController.getContext();
Subject subject = Subject.getSubject(acc); Subject subject = Subject.getSubject(acc);
// Allow setMBeanServer iff performed on behalf of the connector server itself try
if (("setMBeanServer").equals(methodName))
{ {
if (subject != null) if ("getMBeanServer".equals(methodName))
throw new SecurityException("Access denied"); throw new SecurityException("Access denied");
if (args[0] == null) // Corresponds to MBeanServer.invoke
throw new IllegalArgumentException("Null MBeanServer"); if (methodName.equals("invoke") && args.length == 4)
checkVulnerableMethods(args);
if (mbs != null) // Allow setMBeanServer iff performed on behalf of the connector server itself
throw new IllegalArgumentException("MBeanServer already initialized"); if (("setMBeanServer").equals(methodName))
{
if (subject != null)
throw new SecurityException("Access denied");
mbs = (MBeanServer) args[0]; if (args[0] == null)
return 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.auth.jmx.AuthenticationProxy; import org.apache.cassandra.auth.jmx.AuthenticationProxy;
import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.JMXServerOptions; 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 a custom authz proxy was created, attach it to the server now.
if (authzProxy != null) if (authzProxy != null)
jmxServer.setMBeanServerForwarder(authzProxy); jmxServer.setMBeanServerForwarder(authzProxy);
else
jmxServer.setMBeanServerForwarder(AuditLogManager.instance.getMBeanServerForwarder());
jmxServer.start(); jmxServer.start();
registry.setRemoteServerStub(server.toStub()); registry.setRemoteServerStub(server.toStub());
@ -274,7 +278,7 @@ public class JMXServerUtils
} }
return String.format(urlTemplate, hostName, port); return String.format(urlTemplate, hostName, port);
} }
private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator
{ {
private static final MethodHandle ctorHandle; 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; package org.apache.cassandra.audit;
import org.junit.After;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.server.RMISocketFactory;
import java.util.Collections; 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.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
@ -36,15 +48,35 @@ import com.datastax.driver.core.exceptions.NoHostAvailableException;
import com.datastax.driver.core.exceptions.SyntaxError; import com.datastax.driver.core.exceptions.SyntaxError;
import net.openhft.chronicle.queue.RollCycles; import net.openhft.chronicle.queue.RollCycles;
import org.apache.cassandra.auth.AuthEvents; 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.DatabaseDescriptor;
import org.apache.cassandra.config.JMXServerOptions;
import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryEvents; import org.apache.cassandra.cql3.QueryEvents;
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.service.StorageService; 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.containsString;
import static org.hamcrest.CoreMatchers.instanceOf; 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.assertEquals;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@ -758,6 +790,108 @@ public class AuditLoggerTest extends CQLTester
assertEquals("/xyz/not/null", AuditLogManager.instance.getAuditLogOptions().archive_command); 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 * 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.rmi.server.RMISocketFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import javax.management.JMX; import javax.management.JMX;
import javax.management.MBeanServerConnection; import javax.management.MBeanServerConnection;
import javax.management.ObjectName; import javax.management.ObjectName;
@ -39,6 +40,11 @@ import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; 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.AuthenticatedUser;
import org.apache.cassandra.auth.CassandraPrincipal; import org.apache.cassandra.auth.CassandraPrincipal;
import org.apache.cassandra.auth.IAuthorizer; 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.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStoreMBean; import org.apache.cassandra.db.ColumnFamilyStoreMBean;
import org.apache.cassandra.utils.JMXServerUtils; import org.apache.cassandra.utils.JMXServerUtils;
import org.assertj.core.api.Assertions;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -64,6 +71,7 @@ public abstract class AbstractJMXAuthTest extends CQLTester
private RoleResource role; private RoleResource role;
private String tableName; private String tableName;
private JMXResource tableMBean; private JMXResource tableMBean;
private Queue<AuditLogEntry> auditLogs;
@Before @Before
public void setup() throws Throwable 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))"); 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", tableMBean = JMXResource.mbean(String.format("org.apache.cassandra.db:type=Tables,keyspace=%s,table=%s",
KEYSPACE, tableName)); KEYSPACE, tableName));
AuditLogManager.instance.enable(DatabaseDescriptor.getAuditLoggingOptions());
auditLogs = ((InMemoryAuditLogger) AuditLogManager.instance.getLogger()).internalQueue();
} }
@Test @Test
@ -157,6 +167,12 @@ public abstract class AbstractJMXAuthTest extends CQLTester
protected static void setupJMXServer(JMXServerOptions jmxServerOptions) throws Exception 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(); jmxServerOptions.jmx_encryption_options.applyConfig();
jmxServer = JMXServerUtils.createJMXServer(jmxServerOptions, "localhost"); jmxServer = JMXServerUtils.createJMXServer(jmxServerOptions, "localhost");
jmxServer.start(); jmxServer.start();
@ -210,6 +226,10 @@ public abstract class AbstractJMXAuthTest extends CQLTester
private void assertAuthorized(MBeanAction action) private void assertAuthorized(MBeanAction action)
{ {
action.execute(); 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) private void assertUnauthorized(MBeanAction action)
@ -222,9 +242,23 @@ public abstract class AbstractJMXAuthTest extends CQLTester
catch (SecurityException e) catch (SecurityException e)
{ {
assertEquals("Access Denied", e.getLocalizedMessage()); 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() private void clearAllPermissions()
{ {
((StubAuthorizer) DatabaseDescriptor.getAuthorizer()).clear(); ((StubAuthorizer) DatabaseDescriptor.getAuthorizer()).clear();