From f978bea272409109e312a27a121f849879650bdb Mon Sep 17 00:00:00 2001 From: Sumanth Pasupuleti Date: Sun, 6 Jun 2021 20:15:41 -0700 Subject: [PATCH] Obfuscate passwords in statements in QueryEvents patch by Sumanth Pasupuleti; reviewed by Ekaterina Dimitrova, Stefan Miklosovic and Vinay Chella for CASSANDRA-16669 --- CHANGES.txt | 1 + doc/source/new/auditlogging.rst | 66 +++---- doc/source/new/fqllogging.rst | 4 + .../cassandra/audit/AuditLogManager.java | 2 +- .../cassandra/cql3/PasswordObfuscator.java | 40 +++++ .../apache/cassandra/cql3/QueryEvents.java | 33 +++- .../cassandra/audit/AuditLoggerAuthTest.java | 37 +++- .../cql3/PasswordObfuscatorTest.java | 162 ++++++++++++++++++ 8 files changed, 298 insertions(+), 47 deletions(-) create mode 100644 src/java/org/apache/cassandra/cql3/PasswordObfuscator.java create mode 100644 test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java diff --git a/CHANGES.txt b/CHANGES.txt index 9bc602d8f12c..f534612c24f3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.0-rc2 + * Obfuscate passwords in statements in QueryEvents (CASSANDRA-16669) * Fix queries on empty partitions with static data (CASSANDRA-16686) * Keep python driver in artifacts (CASSANDRA-16700) * Improve AuditLogging documentation and logback.xml(CASSANDRA-16682) diff --git a/doc/source/new/auditlogging.rst b/doc/source/new/auditlogging.rst index 84cf8d12b1a8..bf7975819500 100644 --- a/doc/source/new/auditlogging.rst +++ b/doc/source/new/auditlogging.rst @@ -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 +statements is obfuscated as ******* as well as in failed to parse statements. Limitations ^^^^^^^^^^^ @@ -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. diff --git a/doc/source/new/fqllogging.rst b/doc/source/new/fqllogging.rst index daead9c70e51..20085dac8ff5 100644 --- a/doc/source/new/fqllogging.rst +++ b/doc/source/new/fqllogging.rst @@ -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 diff --git a/src/java/org/apache/cassandra/audit/AuditLogManager.java b/src/java/org/apache/cassandra/audit/AuditLogManager.java index bd6d10d289f4..3168b3b4da16 100644 --- a/src/java/org/apache/cassandra/audit/AuditLogManager.java +++ b/src/java/org/apache/cassandra/audit/AuditLogManager.java @@ -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()); } diff --git a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java new file mode 100644 index 000000000000..97ae2e466c71 --- /dev/null +++ b/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; + } +} \ 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 86ef8a56ec83..ffec3ffb4b7b 100644 --- a/src/java/org/apache/cassandra/cql3/QueryEvents.java +++ b/src/java/org/apache/cassandra/cql3/QueryEvents.java @@ -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; @@ -47,12 +48,20 @@ public class QueryEvents private final Set 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); @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -204,8 +217,9 @@ public void notifyPrepareSuccess(Supplier 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) { @@ -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) { @@ -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(); diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java index bbd8561b4791..c64323f4ce41 100644 --- a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java +++ b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java @@ -32,12 +32,14 @@ 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; 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; @@ -80,8 +82,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); @@ -127,6 +129,21 @@ public void testCqlCreateRoleAuditing() 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() { @@ -135,7 +152,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()); } @@ -231,6 +248,10 @@ private static void executeWithCredentials(List queries, String username { //no-op, taken care by caller } + catch (SyntaxError se) + { + // no-op, taken care of by caller + } } if (expectedType != null) @@ -274,19 +295,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()); } } \ 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 new file mode 100644 index 000000000000..23088504089f --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java @@ -0,0 +1,162 @@ +/* + * 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; + +import org.junit.Test; + +import static java.lang.String.format; +import static org.junit.Assert.assertEquals; + +public class PasswordObfuscatorTest +{ + private static final PasswordObfuscator obfuscator = new PasswordObfuscator(); + + @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'")); + } + + @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")); + } + + @Test + public void testCreateRoleWithoutPassword() + { + assertEquals("CREATE ROLE role1", obfuscator.obfuscate("CREATE ROLE role1")); + } + + @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'")); + } + + @Test + public void testAlterRoleWithPassword() + { + assertEquals(format("ALTER ROLE role1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("ALTER ROLE role1 with PASSWORD = '123'")); + } + + @Test + public void testAlterRoleWithPasswordNoSpace() + { + assertEquals(format("ALTER ROLE role1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("ALTER ROLE role1 with PASSWORD='123'")); + } + + @Test + public void testAlterRoleWithPasswordNoImmediateSpace() + { + assertEquals(format("ALTER ROLE role1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("ALTER ROLE role1 with PASSWORD= '123'")); + } + + @Test + public void testAlterRoleWithoutPassword() + { + assertEquals("ALTER ROLE role1", obfuscator.obfuscate("ALTER ROLE role1")); + } + + @Test + public void testCreateUserWithPassword() + { + assertEquals(format("CREATE USER user1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("CREATE USER user1 with PASSWORD '123'")); + } + + @Test + public void testCreateUserWithoutPassword() + { + assertEquals("CREATE USER user1", obfuscator.obfuscate("CREATE USER user1")); + } + + @Test + public void testAlterUserWithPassword() + { + assertEquals(format("ALTER USER user1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("ALTER USER user1 with PASSWORD '123'")); + } + + @Test + public void testAlterUserWithPasswordMixedCase() + { + assertEquals(format("ALTER USER user1 with paSSwoRd%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("ALTER USER user1 with paSSwoRd '123'")); + } + + @Test + public void testAlterUserWithPasswordWithNewLine() + { + assertEquals(format("ALTER USER user1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("ALTER USER user1 with PASSWORD\n'123'")); + } + + @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'")); + } + + @Test + public void testEmptyPasswordObfuscation() + { + assertEquals(String.format("CREATE USER user1 with PASSWORD%s", PasswordObfuscator.OBFUSCATION_TOKEN), + obfuscator.obfuscate("CREATE USER user1 with PASSWORD ''")); + } + + @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'")); + } + + @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 = 'alice123' and LOGIN = true; \n" + + "APPLY BATCH;")); + } + + @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 = 'alice123' and LOGIN = true; \n" + + " CREATE ROLE alice2 WITH PASSWORD = 'alice123' and LOGIN = true; \n" + + "APPLY BATCH;")); + } +} \ No newline at end of file