mirror of https://github.com/apache/cassandra
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:
parent
5dcdd0f048
commit
c853efffa8
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ public class CassandraPrincipal implements Principal, Serializable
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return ("CassandraPrincipal: " + name);
|
||||
return ("CassandraPrincipal: " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{}
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue