This is an automated email from the ASF dual-hosted git repository.

frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new c853efffa8 Support audit logging for JMX operations
c853efffa8 is described below

commit c853efffa8b173a3afe1b966456bb77db5a68883
Author: Abe Ratnofsky <a...@aber.io>
AuthorDate: Fri Dec 6 13:41:31 2024 -0500

    Support audit logging for JMX operations
    
    Patch by Abe Ratnofsky; reviewed by Bernardo Botella, Doug Rohrer, 
Francisco Guerrero for CASSANDRA-20128
---
 CHANGES.txt                                        |   1 +
 build.xml                                          |   2 +-
 .../cassandra/audit/AuditLogEntryCategory.java     |   2 +-
 .../apache/cassandra/audit/AuditLogEntryType.java  |   3 +-
 .../org/apache/cassandra/audit/AuditLogFilter.java |  16 ++-
 .../apache/cassandra/audit/AuditLogManager.java    | 131 ++++++++++++++++++-
 .../apache/cassandra/auth/CassandraPrincipal.java  |   2 +-
 .../cassandra/auth/jmx/AuthorizationProxy.java     |  56 ++++++---
 .../org/apache/cassandra/utils/JMXServerUtils.java |   6 +-
 .../cassandra/utils/JmxInvocationListener.java     |  50 ++++++++
 .../apache/cassandra/audit/AuditLoggerTest.java    | 138 ++++++++++++++++++++-
 .../cassandra/auth/jmx/AbstractJMXAuthTest.java    |  34 +++++
 12 files changed, 411 insertions(+), 30 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 797312bf01..cf59421428 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -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)
diff --git a/build.xml b/build.xml
index 45af046260..ccc38717d2 100644
--- a/build.xml
+++ b/build.xml
@@ -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.
diff --git a/src/java/org/apache/cassandra/audit/AuditLogEntryCategory.java 
b/src/java/org/apache/cassandra/audit/AuditLogEntryCategory.java
index 9db4ce05e9..b848440607 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogEntryCategory.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogEntryCategory.java
@@ -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
 }
diff --git a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java 
b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
index 17d4c98fea..484895a21b 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java
@@ -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;
 
diff --git a/src/java/org/apache/cassandra/audit/AuditLogFilter.java 
b/src/java/org/apache/cassandra/audit/AuditLogFilter.java
index d240e78c83..b775ac7785 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogFilter.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogFilter.java
@@ -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
diff --git a/src/java/org/apache/cassandra/audit/AuditLogManager.java 
b/src/java/org/apache/cassandra/audit/AuditLogManager.java
index 0f49a54060..5d82bd9453 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogManager.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogManager.java
@@ -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;
+    }
 }
diff --git a/src/java/org/apache/cassandra/auth/CassandraPrincipal.java 
b/src/java/org/apache/cassandra/auth/CassandraPrincipal.java
index 41de802a27..8dd0f5a8c2 100644
--- a/src/java/org/apache/cassandra/auth/CassandraPrincipal.java
+++ b/src/java/org/apache/cassandra/auth/CassandraPrincipal.java
@@ -84,7 +84,7 @@ public class CassandraPrincipal implements Principal, 
Serializable
     @Override
     public String toString()
     {
-        return ("CassandraPrincipal:  " + name);
+        return ("CassandraPrincipal: " + name);
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java 
b/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
index c6ea2d90a1..75576f6aef 100644
--- a/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
+++ b/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
@@ -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 (authorize(subject, methodName, args))
-            return invoke(method, args);
+                if (mbs != null)
+                    throw new IllegalArgumentException("MBeanServer already 
initialized");
 
-        throw new SecurityException("Access Denied");
+                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;
+        }
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/utils/JMXServerUtils.java 
b/src/java/org/apache/cassandra/utils/JMXServerUtils.java
index be8f943d2b..0d61f2877d 100644
--- a/src/java/org/apache/cassandra/utils/JMXServerUtils.java
+++ b/src/java/org/apache/cassandra/utils/JMXServerUtils.java
@@ -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;
diff --git a/src/java/org/apache/cassandra/utils/JmxInvocationListener.java 
b/src/java/org/apache/cassandra/utils/JmxInvocationListener.java
new file mode 100644
index 0000000000..cee473cc5f
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/JmxInvocationListener.java
@@ -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)
+    {}
+}
diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java 
b/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
index 6cfaa52e64..46345d88fc 100644
--- a/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
+++ b/test/unit/org/apache/cassandra/audit/AuditLoggerTest.java
@@ -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
      */
diff --git a/test/unit/org/apache/cassandra/auth/jmx/AbstractJMXAuthTest.java 
b/test/unit/org/apache/cassandra/auth/jmx/AbstractJMXAuthTest.java
index 0d75ce6a62..625795c10a 100644
--- a/test/unit/org/apache/cassandra/auth/jmx/AbstractJMXAuthTest.java
+++ b/test/unit/org/apache/cassandra/auth/jmx/AbstractJMXAuthTest.java
@@ -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();


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to