Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cassandra 16669 4.0.0 #140

Open
wants to merge 3 commits into
base: cassandra-4.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 63 additions & 63 deletions .circleci/config.yml

Large diffs are not rendered by default.

66 changes: 34 additions & 32 deletions doc/source/new/auditlogging.rst
Expand Up @@ -89,6 +89,8 @@ Audit logging does not log:

1. Configuration changes made in ``cassandra.yaml``
2. Nodetool Commands
3. Passwords mentioned as part of DCL statements. Instead everything after the appearance of the word password in DCL

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we also add and also failed to parse statements?

statements is obfuscated as *******

Limitations
^^^^^^^^^^^
Expand Down Expand Up @@ -449,38 +451,38 @@ The ``auditlogviewer`` tool is used to dump audit logs. Run the ``auditlogviewer
::

[ec2-user@ip-10-0-2-238 hourly]$ auditlogviewer /cassandra/audit/logs/hourly
WARN 03:12:11,124 Using Pauser.sleepy() as not enough processors, have 2, needs 8+
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427328|type :USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE AuditLogKeyspace;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427329|type :USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE "auditlogkeyspace"
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711446279|type :SELECT|category:QUERY|ks:auditlogkeyspace|scope:t|operation:SELECT * FROM t;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564713878834|type :DROP_TABLE|category:DDL|ks:auditlogkeyspace|scope:t|operation:DROP TABLE IF EXISTS
AuditLogKeyspace.t;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42382|timestamp:1564714618360|ty
pe:REQUEST_FAILURE|category:ERROR|operation:CREATE KEYSPACE AuditLogKeyspace
WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};; Cannot add
existing keyspace "auditlogkeyspace"
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714690968|type :DROP_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:DROP KEYSPACE AuditLogKeyspace;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42406|timestamp:1564714708329|ty pe:CREATE_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:CREATE KEYSPACE
AuditLogKeyspace
WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714870678|type :USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE auditlogkeyspace;
[ec2-user@ip-10-0-2-238 hourly]$
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427328|type:USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE AuditLogKeyspace;
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427329|type:USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE "auditlogkeyspace"
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711446279|type:SELECT|category:QUERY|ks:auditlogkeyspace|scope:t|operation:SELECT * FROM t;
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564713878834|type:DROP_TABLE|category:DDL|ks:auditlogkeyspace|scope:t|operation:DROP TABLE IF EXISTS
AuditLogKeyspace.t;
Type: audit
LogMessage: user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42382|timestamp:1564714618360|type:REQUEST_FAILURE|category:ERROR|operation:CREATE KEYSPACE AuditLogKeyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};; Cannot add existing keyspace "auditlogkeyspace"
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714690968|type:DROP_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:DROP KEYSPACE AuditLogKeyspace;
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42406|timestamp:1564714708329|type:CREATE_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:CREATE KEYSPACE AuditLogKeyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714870678|type:USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE auditlogkeyspace;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630496708|type:CREATE_ROLE|category:DCL|operation:CREATE ROLE role1 WITH PASSWORD*******;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630634552|type:ALTER_ROLE|category:DCL|operation:ATLER ROLE role1 WITH PASSWORD*******;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630698686|type:CREATE_ROLE|category:DCL|operation:CREATE USER user1 WITH PASSWORD*******;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630747344|type:ALTER_ROLE|category:DCL|operation:ALTER USER user1 WITH PASSWORD*******;


The ``auditlogviewer`` tool usage syntax is as follows.
Expand Down
4 changes: 4 additions & 0 deletions doc/source/new/fqllogging.rst
Expand Up @@ -38,6 +38,10 @@ Some of the features of FQL are:
FQL logs all successful Cassandra Query Language (CQL) requests, both events that modify the data and those that query.
While audit logs also include CQL requests, FQL logs only the CQL request. This difference means that FQL can be used to replay or compare logs, which audit logging cannot. FQL is useful for debugging, performance benchmarking, testing and auditing CQL queries, while audit logs are useful for compliance.

Currently DCL statements containing passwords are logged for informational purposes but for security reasons they are not available for replay.
Replay of those statements will be unsuccessful operation because everything after the word password in a DCL statement
will be obfuscated as *******.

In performance testing, FQL appears to have little or no overhead in ``WRITE`` only workloads, and a minor overhead in ``MIXED`` workload.

Query information logged
Expand Down
2 changes: 1 addition & 1 deletion src/java/org/apache/cassandra/audit/AuditLogManager.java
Expand Up @@ -135,7 +135,7 @@ else if (e instanceof AuthenticationException)
builder.setType(AuditLogEntryType.REQUEST_FAILURE);
}

builder.appendToOperation(e.getMessage());
builder.appendToOperation(QueryEvents.instance.getObfuscator().obfuscate(e.getMessage()));

log(builder.build());
}
Expand Down
40 changes: 40 additions & 0 deletions src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
@@ -0,0 +1,40 @@
/*
* 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.cql3;

/**
* Obfuscates passwords in a given string
*/
public class PasswordObfuscator
{
public static final String OBFUSCATION_TOKEN = " *******";
private static final String PASSWORD_TOKEN = "password";

public String obfuscate(String sourceString)
{
if (null == sourceString)
return null;

int passwordTokenStartIndex = sourceString.toLowerCase().indexOf(PASSWORD_TOKEN);
if (passwordTokenStartIndex < 0)
return sourceString;

return sourceString.substring(0, passwordTokenStartIndex + PASSWORD_TOKEN.length()) + OBFUSCATION_TOKEN;
}
}
33 changes: 27 additions & 6 deletions src/java/org/apache/cassandra/cql3/QueryEvents.java
Expand Up @@ -32,6 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.cql3.statements.AuthenticationStatement;
import org.apache.cassandra.cql3.statements.BatchStatement;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.Message;
Expand All @@ -47,12 +48,20 @@ public class QueryEvents

private final Set<Listener> listeners = new CopyOnWriteArraySet<>();

private final PasswordObfuscator passwordObfuscator = new PasswordObfuscator();

@VisibleForTesting
public int listenerCount()
{
return listeners.size();
}

@VisibleForTesting
public PasswordObfuscator getObfuscator()
{
return passwordObfuscator;
}

public void registerListener(Listener listener)
{
listeners.add(listener);
Expand All @@ -72,8 +81,9 @@ public void notifyQuerySuccess(CQLStatement statement,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.querySuccess(statement, query, options, state, queryTime, response);
listener.querySuccess(statement, possiblyObfuscatedQuery, options, state, queryTime, response);
}
catch (Throwable t)
{
Expand All @@ -90,8 +100,9 @@ public void notifyQueryFailure(CQLStatement statement,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.queryFailure(statement, query, options, state, cause);
listener.queryFailure(statement, possiblyObfuscatedQuery, options, state, cause);
}
catch (Throwable t)
{
Expand All @@ -109,8 +120,9 @@ public void notifyExecuteSuccess(CQLStatement statement,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.executeSuccess(statement, query, options, state, queryTime, response);
listener.executeSuccess(statement, possiblyObfuscatedQuery, options, state, queryTime, response);
}
catch (Throwable t)
{
Expand All @@ -128,8 +140,9 @@ public void notifyExecuteFailure(QueryHandler.Prepared prepared,
String query = prepared != null ? prepared.rawCQLStatement : null;
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.executeFailure(statement, query, options, state, cause);
listener.executeFailure(statement, possiblyObfuscatedQuery, options, state, cause);
}
catch (Throwable t)
{
Expand Down Expand Up @@ -204,8 +217,9 @@ public void notifyPrepareSuccess(Supplier<QueryHandler.Prepared> preparedProvide
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(prepared.statement, query) : query;
for (Listener listener : listeners)
listener.prepareSuccess(prepared.statement, query, state, queryTime, response);
listener.prepareSuccess(prepared.statement, possiblyObfuscatedQuery, state, queryTime, response);
}
catch (Throwable t)
{
Expand All @@ -225,8 +239,9 @@ public void notifyPrepareFailure(@Nullable CQLStatement statement, String query,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.prepareFailure(statement, query, state, cause);
listener.prepareFailure(statement, possiblyObfuscatedQuery, state, cause);
}
catch (Throwable t)
{
Expand All @@ -235,6 +250,12 @@ public void notifyPrepareFailure(@Nullable CQLStatement statement, String query,
}
}

private String possiblyObfuscateQuery(CQLStatement statement, String query)
{
// Statement might be null as side-effect of failed parsing, originates from QueryMessage#execute
return null == statement || statement instanceof AuthenticationStatement ? passwordObfuscator.obfuscate(query) : query;
}

public boolean hasListeners()
{
return !listeners.isEmpty();
Expand Down
17 changes: 9 additions & 8 deletions test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
Expand Up @@ -38,6 +38,7 @@
import org.apache.cassandra.config.OverrideConfigurationLoader;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.PasswordObfuscator;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.EmbeddedCassandraService;

Expand Down Expand Up @@ -80,8 +81,8 @@ public static void setup() throws Exception
embedded.start();

executeWithCredentials(
Arrays.asList(getCreateRoleCql(TEST_USER, true, false),
getCreateRoleCql("testuser_nologin", false, false),
Arrays.asList(getCreateRoleCql(TEST_USER, true, false, false),
getCreateRoleCql("testuser_nologin", false, false, false),
"CREATE KEYSPACE testks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}",
"CREATE TABLE testks.table1 (key text PRIMARY KEY, col1 int, col2 int)"),
"cassandra", "cassandra", null);
Expand Down Expand Up @@ -135,7 +136,7 @@ public void testCqlALTERRoleAuditing()
executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
assertTrue(getInMemAuditLogger().size() > 0);
AuditLogEntry logEntry = getInMemAuditLogger().poll();
assertLogEntry(logEntry, AuditLogEntryType.ALTER_ROLE, cql, CASS_USER);
assertLogEntry(logEntry, AuditLogEntryType.ALTER_ROLE, "ALTER ROLE " + TEST_ROLE + " WITH PASSWORD" + PasswordObfuscator.OBFUSCATION_TOKEN, CASS_USER);
assertEquals(0, getInMemAuditLogger().size());
}

Expand Down Expand Up @@ -274,19 +275,19 @@ private static void assertSource(AuditLogEntry logEntry, String username)
assertEquals(username, logEntry.getUser());
}

private static String getCreateRoleCql(String role, boolean login, boolean superUser)
private static String getCreateRoleCql(String role, boolean login, boolean superUser, boolean isPasswordObfuscated)
{
return String.format("CREATE ROLE IF NOT EXISTS %s WITH LOGIN = %s AND SUPERUSER = %s AND PASSWORD = '%s'",
role, login, superUser, TEST_PW);
String baseQueryString = String.format("CREATE ROLE IF NOT EXISTS %s WITH LOGIN = %s AND SUPERUSER = %s AND PASSWORD", role, login, superUser);
return isPasswordObfuscated ? baseQueryString + PasswordObfuscator.OBFUSCATION_TOKEN : baseQueryString + String.format(" = '%s'", TEST_PW);
}

private static void createTestRole()
{
String createTestRoleCQL = getCreateRoleCql(TEST_ROLE, true, false);
String createTestRoleCQL = getCreateRoleCql(TEST_ROLE, true, false, false);
executeWithCredentials(Arrays.asList(createTestRoleCQL), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
assertTrue(getInMemAuditLogger().size() > 0);
AuditLogEntry logEntry = getInMemAuditLogger().poll();
assertLogEntry(logEntry, AuditLogEntryType.CREATE_ROLE, createTestRoleCQL, CASS_USER);
assertLogEntry(logEntry, AuditLogEntryType.CREATE_ROLE, getCreateRoleCql(TEST_ROLE, true, false, true), CASS_USER);
assertEquals(0, getInMemAuditLogger().size());
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add a test case covering an invalid DCL statement, something that you identified in review? if it is already added, ignore my comment.

INFO [Native-Transport-Requests-1] 2021-06-09 23:38:05,965 FileAuditLogger.java:51 - user:anonymous|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:63360|timestamp:1623296285965|type:REQUEST_FAILURE|category:ERROR|operation:BEGIN BATCH CREATE ROLE alice WITH PASSWORD = 'password_a' AND LOGIN = true; CREATE ROLE new_role; APPLY BATCH;; line 1:12 mismatched input 'CREATE' expecting K_APPLY (BEGIN BATCH [CREATE]...)

a rough test case that could cover this case


diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
index 7f227af352..9ea39c26f6 100644
--- a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
+++ b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
@@ -32,6 +32,7 @@ import org.junit.runner.RunWith;
 import com.datastax.driver.core.Cluster;
 import com.datastax.driver.core.Session;
 import com.datastax.driver.core.exceptions.AuthenticationException;
+import com.datastax.driver.core.exceptions.SyntaxError;
 import com.datastax.driver.core.exceptions.UnauthorizedException;
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -128,6 +129,18 @@ public class AuditLoggerAuthTest
         createTestRole();
     }
 
+    @Test
+    public void testCqlCreateRoleSyntaxError()
+    {
+        String createTestRoleCQL = String.format("CREATE ROLE %s WITH LOGIN = %s ANDSUPERUSER = %s AND PASSWORD", TEST_ROLE, true, false) +CASS_PW;
+        String createTestRoleCQLExpected = String.format("CREATE ROLE %s WITH LOGIN = %s ANDSUPERUSER = %s AND PASSWORD", TEST_ROLE, true, false) +PasswordObfuscator.OBFUSCATION_TOKEN;
+        executeWithCredentials(Arrays.asList(createTestRoleCQL), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
+        assertTrue(getInMemAuditLogger().size() > 0);
+        AuditLogEntry logEntry = getInMemAuditLogger().poll();
+        assertLogEntry(logEntry, AuditLogEntryType.REQUEST_FAILURE, createTestRoleCQLExpected, CASS_USER);
+        assertEquals(0, getInMemAuditLogger().size());
+    }
+
     @Test
     public void testCqlALTERRoleAuditing()
     {
@@ -232,6 +245,11 @@ public class AuditLoggerAuthTest
             {
                 //no-op, taken care by caller
             }
+            catch (SyntaxError e)
+            {
+                //no-op, taken care by caller
+
+            }
         }
 
         if (expectedType != null)