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

bereng pushed a commit to branch cassandra-4.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-4.0 by this push:
     new 85248da  PasswordObfuscator should not assume PASSWORD is the last 
item in the WITH clause
85248da is described below

commit 85248da628770d9d93fdd2cbd1eedd55b3ddc206
Author: Bereng <[email protected]>
AuthorDate: Thu Oct 28 09:44:32 2021 +0200

    PasswordObfuscator should not assume PASSWORD is the last item in the WITH 
clause
    
    patch by Berenguer Blasi; reviewed by Benjamin Lerer, Ekaterina Dimitrova 
for CASSANDRA-16801
---
 CHANGES.txt                                        |   1 +
 NEWS.txt                                           |   9 +-
 doc/modules/cassandra/pages/new/auditlogging.adoc  |  13 +-
 .../apache/cassandra/audit/AuditLogManager.java    |  33 ++++-
 .../apache/cassandra/cql3/PasswordObfuscator.java  |  41 +++++-
 .../org/apache/cassandra/cql3/QueryEvents.java     |  42 +++---
 .../cql3/statements/AlterRoleStatement.java        |   7 +
 .../cql3/statements/AuthenticationStatement.java   |   5 +
 .../cql3/statements/CreateRoleStatement.java       |   7 +
 .../cassandra/audit/AuditLoggerAuthTest.java       |  71 ++++++++--
 .../cassandra/cql3/PasswordObfuscatorTest.java     | 156 +++++++++++++++------
 11 files changed, 296 insertions(+), 89 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 2bb5f30..cf4c206 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -101,6 +101,7 @@ Merged from 3.11:
 Merged from 3.0:
 
 4.0-rc2
+ * Improved password obfuscation (CASSANDRA-16801)
  * Avoid memoizing the wrong min cluster version during upgrades 
(CASSANDRA-16759)
  * Obfuscate passwords in statements in QueryEvents (CASSANDRA-16669)
  * Fix queries on empty partitions with static data (CASSANDRA-16686)
diff --git a/NEWS.txt b/NEWS.txt
index 649f3fd..749b531 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -37,6 +37,11 @@ using the provided 'sstableupgrade' tool.
 4.0.2
 =====
 
+New features
+------------
+    - Full support for Java 11, it is not experimental anymore.
+    - DCL statements in audit logs will now obscure only the password if they 
don't fail to parse.
+
 Upgrading
 ---------
     - Before you upgrade, if you are using 
`cassandra.auth_bcrypt_gensalt_log2_rounds` property,
@@ -49,10 +54,6 @@ Upgrading
       currently both old and new names should work. Cassandra 4.0.0 and 
Cassandra 4.0.1 work ONLY with the new names
       (They weren't updated in cassandra.yaml though).
 
-New Features
-------------
-    - Full support for Java 11, it is not experimental anymore.
-      
 4.0
 ===
 
diff --git a/doc/modules/cassandra/pages/new/auditlogging.adoc 
b/doc/modules/cassandra/pages/new/auditlogging.adoc
index e81776a..a479921 100644
--- a/doc/modules/cassandra/pages/new/auditlogging.adoc
+++ b/doc/modules/cassandra/pages/new/auditlogging.adoc
@@ -32,6 +32,9 @@ The audit log does not contain:
 
 * configuration changes made in `cassandra.yaml` file
 * `nodetool` commands
+* Passwords mentioned as part of DCL statements: Passwords will be obfuscated 
as \*\*\*\*\*\*\*.
+ ** Statements that fail to parse will have everything after the appearance of 
the word password obfuscated as \*\*\*\*\*\*\*.
+ ** Statements with a mistyped word 'password' will be logged without 
obfuscation. Please make sure to use a different password on retries.
 
 The audit log is a series of log entries. 
 An audit log entry contains:
@@ -455,7 +458,15 @@ 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]$
+
+Password obfuscation examples:
+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 '*******';
 ----
 
 == Diagnostic events for user audit logging
diff --git a/src/java/org/apache/cassandra/audit/AuditLogManager.java 
b/src/java/org/apache/cassandra/audit/AuditLogManager.java
index 3168b3b..88e0251 100644
--- a/src/java/org/apache/cassandra/audit/AuditLogManager.java
+++ b/src/java/org/apache/cassandra/audit/AuditLogManager.java
@@ -22,12 +22,13 @@ import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,12 +36,14 @@ import org.apache.cassandra.auth.AuthEvents;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.config.ParameterizedClass;
 import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.PasswordObfuscator;
 import org.apache.cassandra.cql3.QueryEvents;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.statements.BatchStatement;
 import org.apache.cassandra.exceptions.AuthenticationException;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.exceptions.PreparedQueryNotFoundException;
+import org.apache.cassandra.exceptions.SyntaxException;
 import org.apache.cassandra.exceptions.UnauthorizedException;
 import org.apache.cassandra.service.QueryState;
 import org.apache.cassandra.transport.Message;
@@ -120,6 +123,11 @@ public class AuditLogManager implements 
QueryEvents.Listener, AuthEvents.Listene
 
     private void log(AuditLogEntry logEntry, Exception e)
     {
+        log(logEntry, e, null);
+    }
+
+    private void log(AuditLogEntry logEntry, Exception e, List<String> queries)
+    {
         AuditLogEntry.Builder builder = new AuditLogEntry.Builder(logEntry);
 
         if (e instanceof UnauthorizedException)
@@ -135,7 +143,7 @@ public class AuditLogManager implements 
QueryEvents.Listener, AuthEvents.Listene
             builder.setType(AuditLogEntryType.REQUEST_FAILURE);
         }
 
-        
builder.appendToOperation(QueryEvents.instance.getObfuscator().obfuscate(e.getMessage()));
+        builder.appendToOperation(obfuscatePasswordInformation(e, queries));
 
         log(builder.build());
     }
@@ -206,7 +214,7 @@ public class AuditLogManager implements 
QueryEvents.Listener, AuthEvents.Listene
         AuditLogEntry entry = new 
AuditLogEntry.Builder(state).setOperation(query)
                                                               
.setOptions(options)
                                                               .build();
-        log(entry, cause);
+        log(entry, cause, query == null ? null : ImmutableList.of(query));
     }
 
     public void executeSuccess(CQLStatement statement, String query, 
QueryOptions options, QueryState state, long queryTime, Message.Response 
response)
@@ -240,7 +248,7 @@ public class AuditLogManager implements 
QueryEvents.Listener, AuthEvents.Listene
                                                                   .build();
         }
         if (entry != null)
-            log(entry, cause);
+            log(entry, cause, query == null ? null : ImmutableList.of(query));
     }
 
     public void batchSuccess(BatchStatement.Type batchType, List<? extends 
CQLStatement> statements, List<String> queries, List<List<ByteBuffer>> values, 
QueryOptions options, QueryState state, long queryTime, Message.Response 
response)
@@ -259,7 +267,7 @@ public class AuditLogManager implements 
QueryEvents.Listener, AuthEvents.Listene
                                                               
.setOptions(options)
                                                               
.setType(AuditLogEntryType.BATCH)
                                                               .build();
-        log(entry, cause);
+        log(entry, cause, queries);
     }
 
     private static List<AuditLogEntry> buildEntriesForBatch(List<? extends 
CQLStatement> statements, List<String> queries, QueryState state, QueryOptions 
options, long queryStartTimeMillis)
@@ -328,4 +336,19 @@ public class AuditLogManager implements 
QueryEvents.Listener, AuthEvents.Listene
                                                               .build();
         log(entry, cause);
     }
+
+    private String obfuscatePasswordInformation(Exception e, List<String> 
queries)
+    {
+        // A syntax error may reveal the password in the form of 'line 1:33 
mismatched input 'secret_password''
+        if (e instanceof SyntaxException && queries != null && 
!queries.isEmpty())
+        {
+            for (String query : queries)
+            {
+                if 
(query.toLowerCase().contains(PasswordObfuscator.PASSWORD_TOKEN))
+                    return "Syntax Exception. Obscured for security reasons.";
+            }
+        }
+
+        return PasswordObfuscator.obfuscate(e.getMessage());
+    }
 }
diff --git a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java 
b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
index 97ae2e4..89962f9 100644
--- a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
+++ b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
@@ -18,15 +18,26 @@
 
 package org.apache.cassandra.cql3;
 
+import com.google.common.base.Optional;
+
+import org.apache.cassandra.auth.PasswordAuthenticator;
+import org.apache.cassandra.auth.RoleOptions;
+
 /**
  * Obfuscates passwords in a given string
  */
 public class PasswordObfuscator
 {
-    public static final String OBFUSCATION_TOKEN = " *******";
-    private static final String PASSWORD_TOKEN = "password";
+    public static final String OBFUSCATION_TOKEN = "*******";
+    public static final String PASSWORD_TOKEN = 
PasswordAuthenticator.PASSWORD_KEY.toLowerCase();
 
-    public String obfuscate(String sourceString)
+    /**
+     * Obfuscates everything after the first appearance password token
+     * 
+     * @param sourceString The query to obfuscate
+     * @return The obfuscated query
+     */
+    public static String obfuscate(String sourceString)
     {
         if (null == sourceString)
             return null;
@@ -35,6 +46,26 @@ public class PasswordObfuscator
         if (passwordTokenStartIndex < 0)
             return sourceString;
 
-        return sourceString.substring(0, passwordTokenStartIndex + 
PASSWORD_TOKEN.length()) + OBFUSCATION_TOKEN;
+        return sourceString.substring(0, passwordTokenStartIndex + 
PASSWORD_TOKEN.length()) + " " + OBFUSCATION_TOKEN;
+    }
+
+    /**
+     * Obfuscates the password in a query
+     * 
+     * @param query The query whose password to obfuscate
+     * @param opts The options containing the password to obfuscate
+     * @return The query with obfuscated password
+     */
+    public static String obfuscate(String query, RoleOptions opts)
+    {
+        if (opts == null || query == null || query.isEmpty())
+            return query;
+
+        Optional<String> pass = opts.getPassword();
+        if (!pass.isPresent() || pass.get().isEmpty())
+            return query;
+
+        // match new line, case insensitive (?si), and PASSWORD_TOKEN up to 
the actual password greedy. Group that and replace the password
+        return query.replaceAll("((?si)"+ PASSWORD_TOKEN + ".+?)" + 
pass.get(), "$1" + PasswordObfuscator.OBFUSCATION_TOKEN);
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/cql3/QueryEvents.java 
b/src/java/org/apache/cassandra/cql3/QueryEvents.java
index ffec3ff..52414eb 100644
--- a/src/java/org/apache/cassandra/cql3/QueryEvents.java
+++ b/src/java/org/apache/cassandra/cql3/QueryEvents.java
@@ -48,20 +48,12 @@ 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);
@@ -81,9 +73,9 @@ public class QueryEvents
     {
         try
         {
-            final String possiblyObfuscatedQuery = listeners.size() > 0 ? 
possiblyObfuscateQuery(statement, query) : query;
+            final String maybeObfuscatedQuery = listeners.size() > 0 ? 
maybeObfuscatePassword(statement, query) : query;
             for (Listener listener : listeners)
-                listener.querySuccess(statement, possiblyObfuscatedQuery, 
options, state, queryTime, response);
+                listener.querySuccess(statement, maybeObfuscatedQuery, 
options, state, queryTime, response);
         }
         catch (Throwable t)
         {
@@ -100,9 +92,9 @@ public class QueryEvents
     {
         try
         {
-            final String possiblyObfuscatedQuery = listeners.size() > 0 ? 
possiblyObfuscateQuery(statement, query) : query;
+            final String maybeObfuscatedQuery = listeners.size() > 0 ? 
maybeObfuscatePassword(statement, query) : query;
             for (Listener listener : listeners)
-                listener.queryFailure(statement, possiblyObfuscatedQuery, 
options, state, cause);
+                listener.queryFailure(statement, maybeObfuscatedQuery, 
options, state, cause);
         }
         catch (Throwable t)
         {
@@ -120,9 +112,9 @@ public class QueryEvents
     {
         try
         {
-            final String possiblyObfuscatedQuery = listeners.size() > 0 ? 
possiblyObfuscateQuery(statement, query) : query;
+            final String maybeObfuscatedQuery = listeners.size() > 0 ? 
maybeObfuscatePassword(statement, query) : query;
             for (Listener listener : listeners)
-                listener.executeSuccess(statement, possiblyObfuscatedQuery, 
options, state, queryTime, response);
+                listener.executeSuccess(statement, maybeObfuscatedQuery, 
options, state, queryTime, response);
         }
         catch (Throwable t)
         {
@@ -140,9 +132,9 @@ public class QueryEvents
         String query = prepared != null ? prepared.rawCQLStatement : null;
         try
         {
-            final String possiblyObfuscatedQuery = listeners.size() > 0 ? 
possiblyObfuscateQuery(statement, query) : query;
+            final String maybeObfuscatedQuery = listeners.size() > 0 ? 
maybeObfuscatePassword(statement, query) : query;
             for (Listener listener : listeners)
-                listener.executeFailure(statement, possiblyObfuscatedQuery, 
options, state, cause);
+                listener.executeFailure(statement, maybeObfuscatedQuery, 
options, state, cause);
         }
         catch (Throwable t)
         {
@@ -217,9 +209,9 @@ public class QueryEvents
             {
                 try
                 {
-                    final String possiblyObfuscatedQuery = listeners.size() > 
0 ? possiblyObfuscateQuery(prepared.statement, query) : query;
+                    final String maybeObfuscatedQuery = listeners.size() > 0 ? 
maybeObfuscatePassword(prepared.statement, query) : query;
                     for (Listener listener : listeners)
-                        listener.prepareSuccess(prepared.statement, 
possiblyObfuscatedQuery, state, queryTime, response);
+                        listener.prepareSuccess(prepared.statement, 
maybeObfuscatedQuery, state, queryTime, response);
                 }
                 catch (Throwable t)
                 {
@@ -239,9 +231,9 @@ public class QueryEvents
     {
         try
         {
-            final String possiblyObfuscatedQuery = listeners.size() > 0 ? 
possiblyObfuscateQuery(statement, query) : query;
+            final String maybeObfuscatedQuery = listeners.size() > 0 ? 
maybeObfuscatePassword(statement, query) : query;
             for (Listener listener : listeners)
-                listener.prepareFailure(statement, possiblyObfuscatedQuery, 
state, cause);
+                listener.prepareFailure(statement, maybeObfuscatedQuery, 
state, cause);
         }
         catch (Throwable t)
         {
@@ -250,10 +242,16 @@ public class QueryEvents
         }
     }
 
-    private String possiblyObfuscateQuery(CQLStatement statement, String query)
+    private String maybeObfuscatePassword(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;
+        if (statement == null)
+            return PasswordObfuscator.obfuscate(query);
+
+        if (statement instanceof AuthenticationStatement)
+             return ((AuthenticationStatement) 
statement).obfuscatePassword(query);
+
+        return query;
     }
 
     public boolean hasListeners()
diff --git 
a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
index 7a748e8..2ffd050 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
@@ -22,6 +22,7 @@ import org.apache.cassandra.audit.AuditLogEntryType;
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.auth.IRoleManager.Option;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.PasswordObfuscator;
 import org.apache.cassandra.cql3.RoleName;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.service.ClientState;
@@ -117,4 +118,10 @@ public class AlterRoleStatement extends 
AuthenticationStatement
     {
         return new AuditLogContext(AuditLogEntryType.ALTER_ROLE);
     }
+
+    @Override
+    public String obfuscatePassword(String query)
+    {
+        return PasswordObfuscator.obfuscate(query, opts);
+    }
 }
diff --git 
a/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java
index a8cbaa7..db3aa99 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AuthenticationStatement.java
@@ -63,5 +63,10 @@ public abstract class AuthenticationStatement extends 
CQLStatement.Raw implement
                                                           
state.getUser().getName()));
         }
     }
+
+    public String obfuscatePassword(String query)
+    {
+        return query;
+    }
 }
 
diff --git 
a/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java 
b/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java
index 8224c4b..b3333fc 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateRoleStatement.java
@@ -21,6 +21,7 @@ import org.apache.cassandra.audit.AuditLogContext;
 import org.apache.cassandra.audit.AuditLogEntryType;
 import org.apache.cassandra.auth.*;
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.PasswordObfuscator;
 import org.apache.cassandra.cql3.RoleName;
 import org.apache.cassandra.exceptions.*;
 import org.apache.cassandra.service.ClientState;
@@ -125,4 +126,10 @@ public class CreateRoleStatement extends 
AuthenticationStatement
     {
         return new AuditLogContext(AuditLogEntryType.CREATE_ROLE);
     }
+
+    @Override
+    public String obfuscatePassword(String query)
+    {
+        return PasswordObfuscator.obfuscate(query, opts);
+    }
 }
diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java 
b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
index a873b6e..963a473 100644
--- a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
+++ b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
@@ -41,6 +41,7 @@ import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.PasswordObfuscator;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.service.EmbeddedCassandraService;
+import org.hamcrest.CoreMatchers;
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
@@ -132,13 +133,13 @@ public class AuditLoggerAuthTest
     {
         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",
+        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);
+        assertLogEntry(logEntry, AuditLogEntryType.REQUEST_FAILURE, 
createTestRoleCQLExpected, CASS_USER,  TEST_PW);
         assertEquals(0, getInMemAuditLogger().size());
     }
 
@@ -150,7 +151,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.ALTER_ROLE, "ALTER ROLE " + 
TEST_ROLE + " WITH PASSWORD" + PasswordObfuscator.OBFUSCATION_TOKEN, CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.ALTER_ROLE, "ALTER ROLE " + 
TEST_ROLE + " WITH PASSWORD = '" + PasswordObfuscator.OBFUSCATION_TOKEN + "'", 
CASS_USER, "foo_bar");
         assertEquals(0, getInMemAuditLogger().size());
     }
 
@@ -162,7 +163,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.DROP_ROLE, cql, CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.DROP_ROLE, cql, CASS_USER, 
"");
         assertEquals(0, getInMemAuditLogger().size());
     }
 
@@ -173,7 +174,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.LIST_ROLES, cql, CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.LIST_ROLES, cql, CASS_USER, 
"");
     }
 
     @Test
@@ -183,7 +184,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.LIST_PERMISSIONS, cql, 
CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.LIST_PERMISSIONS, cql, 
CASS_USER, "");
     }
 
     @Test
@@ -194,7 +195,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.GRANT, cql, CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.GRANT, cql, CASS_USER, "");
     }
 
     @Test
@@ -205,7 +206,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.REVOKE, cql, CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.REVOKE, cql, CASS_USER, "");
     }
 
     @Test
@@ -216,10 +217,49 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(cql), TEST_USER, TEST_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.UNAUTHORIZED_ATTEMPT, cql, 
TEST_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.UNAUTHORIZED_ATTEMPT, cql, 
TEST_USER, "");
         assertEquals(0, getInMemAuditLogger().size());
     }
 
+    @Test
+    public void testCqlUSERCommandsAuditing()
+    {
+        //CREATE USER and ALTER USER are supported only for backwards 
compatibility.
+
+        String user = TEST_ROLE + "user";
+        String cql = "CREATE USER " + user + " WITH PASSWORD '" + TEST_PW + 
"'";
+        executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
+        assertTrue(getInMemAuditLogger().size() > 0);
+        AuditLogEntry logEntry = getInMemAuditLogger().poll();
+        assertLogEntry(logEntry,
+                       AuditLogEntryType.CREATE_ROLE,
+                       "CREATE USER " + user + " WITH PASSWORD '" + 
PasswordObfuscator.OBFUSCATION_TOKEN + "'",
+                       CASS_USER,
+                       TEST_PW);
+
+        cql = "ALTER USER " + user + " WITH PASSWORD '" + TEST_PW + "'";
+        executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
+        assertTrue(getInMemAuditLogger().size() > 0);
+        logEntry = getInMemAuditLogger().poll();
+        assertLogEntry(logEntry,
+                       AuditLogEntryType.ALTER_ROLE,
+                       "ALTER USER " + user + " WITH PASSWORD '" + 
PasswordObfuscator.OBFUSCATION_TOKEN + "'",
+                       CASS_USER,
+                       TEST_PW);
+
+        cql = "ALTER USER " + user + " WITH PASSWORD " + TEST_PW;
+        executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, 
AuditLogEntryType.LOGIN_SUCCESS);
+        assertTrue(getInMemAuditLogger().size() > 0);
+        logEntry = getInMemAuditLogger().poll();
+        assertLogEntry(logEntry,
+                       AuditLogEntryType.REQUEST_FAILURE,
+                       "ALTER USER " + user
+                       + " WITH PASSWORD " + 
PasswordObfuscator.OBFUSCATION_TOKEN
+                       + "; Syntax Exception. Obscured for security reasons.",
+                       CASS_USER,
+                       TEST_PW);
+    }
+
     /**
      * Helper methods
      */
@@ -274,7 +314,7 @@ public class AuditLoggerAuthTest
         return ((InMemoryAuditLogger) 
AuditLogManager.instance.getLogger()).inMemQueue;
     }
 
-    private static void assertLogEntry(AuditLogEntry logEntry, 
AuditLogEntryType type, String cql, String username)
+    private static void assertLogEntry(AuditLogEntry logEntry, 
AuditLogEntryType type, String cql, String username, String forbiddenPassword)
     {
         assertSource(logEntry, username);
         assertNotEquals(0, logEntry.getTimestamp());
@@ -282,6 +322,8 @@ public class AuditLoggerAuthTest
         if (null != cql && !cql.isEmpty())
         {
             assertThat(logEntry.getOperation(), containsString(cql));
+            if (!forbiddenPassword.isEmpty())
+                assertThat(logEntry.getOperation(), 
CoreMatchers.not(containsString(forbiddenPassword)));
         }
     }
 
@@ -295,8 +337,11 @@ public class AuditLoggerAuthTest
 
     private static String getCreateRoleCql(String role, boolean login, boolean 
superUser, boolean isPasswordObfuscated)
     {
-        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);
+        return String.format("CREATE ROLE IF NOT EXISTS %s WITH PASSWORD = 
'%s' AND LOGIN = %s AND SUPERUSER = %s",
+                             role,
+                             isPasswordObfuscated ? 
PasswordObfuscator.OBFUSCATION_TOKEN : TEST_PW,
+                             login,
+                             superUser);
     }
 
     private static void createTestRole()
@@ -305,7 +350,7 @@ public class AuditLoggerAuthTest
         executeWithCredentials(Arrays.asList(createTestRoleCQL), CASS_USER, 
CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
         assertTrue(getInMemAuditLogger().size() > 0);
         AuditLogEntry logEntry = getInMemAuditLogger().poll();
-        assertLogEntry(logEntry, AuditLogEntryType.CREATE_ROLE, 
getCreateRoleCql(TEST_ROLE, true, false, true), CASS_USER);
+        assertLogEntry(logEntry, AuditLogEntryType.CREATE_ROLE, 
getCreateRoleCql(TEST_ROLE, true, false, true), CASS_USER, "");
         assertEquals(0, getInMemAuditLogger().size());
     }
 }
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java 
b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
index 2308850..09a366e 100644
--- a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
+++ b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
@@ -18,145 +18,223 @@
 
 package org.apache.cassandra.cql3;
 
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import org.apache.cassandra.auth.RoleOptions;
+
+import static org.apache.cassandra.cql3.PasswordObfuscator.*;
 import static java.lang.String.format;
 import static org.junit.Assert.assertEquals;
 
 public class PasswordObfuscatorTest
 {
-    private static final PasswordObfuscator obfuscator = new 
PasswordObfuscator();
+    private static final RoleOptions opts = new RoleOptions();
+    private static final String optsPassword = "testpassword";
+
+    @BeforeClass
+    public static void startup()
+    {
+        opts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD, 
"testpassword");
+    }
 
     @Test
     public void testCreatRoleWithLoginPriorToPassword()
     {
-        assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND 
PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE ROLE role1 WITH LOGIN = true 
AND PASSWORD = '123'"));
+        assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND PASSWORD 
%s", OBFUSCATION_TOKEN),
+                     obfuscate("CREATE ROLE role1 WITH LOGIN = true AND 
PASSWORD = '123'"));
+
+        assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND PASSWORD 
= '%s'", OBFUSCATION_TOKEN),
+                     obfuscate(format("CREATE ROLE role1 WITH LOGIN = true AND 
PASSWORD = '%s'", optsPassword), opts));
     }
 
     @Test
     public void testCreatRoleWithLoginAfterPassword()
     {
-        assertEquals(format("CREATE ROLE role1 WITH password%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE ROLE role1 WITH password = 
'123' AND LOGIN = true"));
+        assertEquals(format("CREATE ROLE role1 WITH password %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("CREATE ROLE role1 WITH password = '123' AND 
LOGIN = true"));
+
+        assertEquals(format("CREATE ROLE role1 WITH password = '%s' AND LOGIN 
= true", OBFUSCATION_TOKEN),
+                     obfuscate(format("CREATE ROLE role1 WITH password = '%s' 
AND LOGIN = true", optsPassword), opts));
     }
 
     @Test
     public void testCreateRoleWithoutPassword()
     {
-        assertEquals("CREATE ROLE role1", obfuscator.obfuscate("CREATE ROLE 
role1"));
+        assertEquals("CREATE ROLE role1", obfuscate("CREATE ROLE role1"));
+        assertEquals("CREATE ROLE role1", obfuscate("CREATE ROLE role1", 
opts));
     }
 
     @Test
     public void testCreateMultipleRoles()
     {
-        assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND 
PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE ROLE role1 WITH LOGIN = true 
AND PASSWORD = '123';" +
-                                          "CREATE ROLE role2 WITH LOGIN = true 
AND PASSWORD = '123'"));
+        assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND PASSWORD 
%s", OBFUSCATION_TOKEN),
+                     obfuscate("CREATE ROLE role1 WITH LOGIN = true AND 
PASSWORD = '123';" +
+                                                  "CREATE ROLE role2 WITH 
LOGIN = true AND PASSWORD = '123'"));
+
+        assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND PASSWORD 
= '%s';"
+                            + "CREATE ROLE role2 WITH LOGIN = true AND 
PASSWORD = '%s'", OBFUSCATION_TOKEN, OBFUSCATION_TOKEN),
+                     obfuscate(format("CREATE ROLE role1 WITH LOGIN = true AND 
PASSWORD = '%s';"
+                                                         + "CREATE ROLE role2 
WITH LOGIN = true AND PASSWORD = '%s'", optsPassword, optsPassword),
+                                                  opts));
     }
 
     @Test
     public void testAlterRoleWithPassword()
     {
-        assertEquals(format("ALTER ROLE role1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("ALTER ROLE role1 with PASSWORD = 
'123'"));
+        assertEquals(format("ALTER ROLE role1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("ALTER ROLE role1 with PASSWORD = '123'"));
+
+        assertEquals(format("ALTER ROLE role1 with PASSWORD = '%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("ALTER ROLE role1 with PASSWORD = '%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testAlterRoleWithPasswordNoSpace()
     {
-        assertEquals(format("ALTER ROLE role1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("ALTER ROLE role1 with 
PASSWORD='123'"));
+        assertEquals(format("ALTER ROLE role1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("ALTER ROLE role1 with PASSWORD='123'"));
+
+        assertEquals(format("ALTER ROLE role1 with PASSWORD='%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("ALTER ROLE role1 with PASSWORD='%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testAlterRoleWithPasswordNoImmediateSpace()
     {
-        assertEquals(format("ALTER ROLE role1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("ALTER ROLE role1 with PASSWORD= 
'123'"));
+        assertEquals(format("ALTER ROLE role1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("ALTER ROLE role1 with PASSWORD= '123'"));
+
+        assertEquals(format("ALTER ROLE role1 with PASSWORD= '%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("ALTER ROLE role1 with PASSWORD= '%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testAlterRoleWithoutPassword()
     {
-        assertEquals("ALTER ROLE role1", obfuscator.obfuscate("ALTER ROLE 
role1"));
+        assertEquals("ALTER ROLE role1", obfuscate("ALTER ROLE role1"));
+
+        assertEquals("ALTER ROLE role1", obfuscate("ALTER ROLE role1", opts));
     }
 
     @Test
     public void testCreateUserWithPassword()
     {
-        assertEquals(format("CREATE USER user1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE USER user1 with PASSWORD 
'123'"));
+        assertEquals(format("CREATE USER user1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("CREATE USER user1 with PASSWORD '123'"));
+
+        assertEquals(format("CREATE USER user1 with PASSWORD '%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("CREATE USER user1 with PASSWORD '%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testCreateUserWithoutPassword()
     {
-        assertEquals("CREATE USER user1", obfuscator.obfuscate("CREATE USER 
user1"));
+        assertEquals("CREATE USER user1", obfuscate("CREATE USER user1"));
+
+        assertEquals("CREATE USER user1", obfuscate("CREATE USER user1", 
opts));
     }
 
     @Test
     public void testAlterUserWithPassword()
     {
-        assertEquals(format("ALTER USER user1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("ALTER USER user1 with PASSWORD 
'123'"));
+        assertEquals(format("ALTER USER user1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("ALTER USER user1 with PASSWORD '123'"));
+
+        assertEquals(format("ALTER USER user1 with PASSWORD '%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("ALTER USER user1 with PASSWORD '%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testAlterUserWithPasswordMixedCase()
     {
-        assertEquals(format("ALTER USER user1 with paSSwoRd%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("ALTER USER user1 with paSSwoRd 
'123'"));
+        assertEquals(format("ALTER USER user1 with paSSwoRd %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("ALTER USER user1 with paSSwoRd '123'"));
+
+        assertEquals(format("ALTER USER user1 with paSSwoRd '%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("ALTER USER user1 with paSSwoRd '%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testAlterUserWithPasswordWithNewLine()
     {
-        assertEquals(format("ALTER USER user1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("ALTER USER user1 with 
PASSWORD\n'123'"));
+        assertEquals(format("ALTER USER user1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("ALTER USER user1 with PASSWORD\n'123'"));
+
+        assertEquals(format("ALTER USER user1 with PASSWORD\n'%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("ALTER USER user1 with PASSWORD\n'%s'", 
optsPassword), opts));
     }
 
     @Test
     public void testPasswordWithNewLinesObfuscation()
     {
-        assertEquals(String.format("CREATE USER user1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE USER user1 with PASSWORD 
'a\nb'"));
+        assertEquals(String.format("CREATE USER user1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("CREATE USER user1 with PASSWORD 'a\nb'"));
+
+        RoleOptions newLinePassOpts = new RoleOptions();
+        
newLinePassOpts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD,
 "test\npassword");
+        assertEquals(String.format("CREATE USER user1 with PASSWORD '%s'", 
OBFUSCATION_TOKEN),
+                     obfuscate(format("CREATE USER user1 with PASSWORD '%s'", 
"test\npassword"), newLinePassOpts));
     }
 
     @Test
     public void testEmptyPasswordObfuscation()
     {
-        assertEquals(String.format("CREATE USER user1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE USER user1 with PASSWORD 
''"));
+        assertEquals(String.format("CREATE USER user1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("CREATE USER user1 with PASSWORD ''"));
+
+        RoleOptions emptyPassOpts = new RoleOptions();
+        
emptyPassOpts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD, 
"");
+        assertEquals("CREATE USER user1 with PASSWORD ''",
+                     obfuscate("CREATE USER user1 with PASSWORD ''", 
emptyPassOpts));
     }
 
     @Test
     public void testPasswordWithSpaces()
     {
-        assertEquals(String.format("CREATE USER user1 with PASSWORD%s", 
PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("CREATE USER user1 with PASSWORD 'p 
a ss wor d'"));
+        assertEquals(String.format("CREATE USER user1 with PASSWORD %s", 
OBFUSCATION_TOKEN),
+                     obfuscate("CREATE USER user1 with PASSWORD 'p a ss wor 
d'"));
     }
 
     @Test
     public void testSimpleBatch()
     {
         assertEquals(format("BEGIN BATCH \n" +
-                            "    CREATE ROLE alice1 WITH PASSWORD%s",
-                            PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("BEGIN BATCH \n" +
+                            "    CREATE ROLE alice1 WITH PASSWORD %s",
+                            OBFUSCATION_TOKEN),
+                     obfuscate("BEGIN BATCH \n" +
                                           "    CREATE ROLE alice1 WITH 
PASSWORD = 'alice123' and LOGIN = true; \n" +
                                           "APPLY BATCH;"));
+
+        assertEquals(format("BEGIN BATCH \n" +
+                            "    CREATE ROLE alice1 WITH PASSWORD = '%s' and 
LOGIN = true; \n" +
+                            "APPLY BATCH;", OBFUSCATION_TOKEN),
+                     obfuscate(format("BEGIN BATCH \n" +
+                                      "    CREATE ROLE alice1 WITH PASSWORD = 
'%s' and LOGIN = true; \n" +
+                                      "APPLY BATCH;", optsPassword),
+                               opts));
     }
 
     @Test
     public void testComplexBatch()
     {
         assertEquals(format("BEGIN BATCH \n" +
-                            "    CREATE ROLE alice1 WITH PASSWORD%s",
-                            PasswordObfuscator.OBFUSCATION_TOKEN),
-                     obfuscator.obfuscate("BEGIN BATCH \n" +
+                            "    CREATE ROLE alice1 WITH PASSWORD %s",
+                            OBFUSCATION_TOKEN),
+                     obfuscate("BEGIN BATCH \n" +
                                           "    CREATE ROLE alice1 WITH 
PASSWORD = 'alice123' and LOGIN = true; \n" +
                                           "    CREATE ROLE alice2 WITH 
PASSWORD = 'alice123' and LOGIN = true; \n" +
                                           "APPLY BATCH;"));
-    }
-}
\ No newline at end of file
+
+        assertEquals(format("BEGIN BATCH \n" +
+                            "    CREATE ROLE alice1 WITH PASSWORD = '%s' and 
LOGIN = true; \n" +
+                            "    CREATE ROLE alice2 WITH PASSWORD = '%s' and 
LOGIN = true; \n" +
+                            "APPLY BATCH;"
+                            , OBFUSCATION_TOKEN, OBFUSCATION_TOKEN),
+                     obfuscate(format("BEGIN BATCH \n" +
+                                      "    CREATE ROLE alice1 WITH PASSWORD = 
'%s' and LOGIN = true; \n" +
+                                      "    CREATE ROLE alice2 WITH PASSWORD = 
'%s' and LOGIN = true; \n" +
+                                      "APPLY BATCH;", optsPassword, 
optsPassword),
+                               opts));
+    }
+}

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to