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
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ public class CassandraPrincipal implements Principal, Serializable
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
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.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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue