This is an automated email from the ASF dual-hosted git repository.
bereng 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 6946b30 Pre hashed passwords in CQL
6946b30 is described below
commit 6946b304e94a8a8d1250680664ddc03b61a139c9
Author: Bereng <[email protected]>
AuthorDate: Tue Mar 1 07:44:19 2022 +0100
Pre hashed passwords in CQL
patch by Berenguer Blasi; reviewed by Andres de la Peña for CASSANDRA-17334
---
CHANGES.txt | 1 +
NEWS.txt | 1 +
pylib/cqlshlib/cql3handling.py | 4 +-
src/antlr/Lexer.g | 1 +
src/antlr/Parser.g | 27 ++-
.../cassandra/auth/CassandraRoleManager.java | 18 +-
.../org/apache/cassandra/auth/IRoleManager.java | 2 +-
.../org/apache/cassandra/auth/RoleOptions.java | 41 ++++-
.../apache/cassandra/cql3/PasswordObfuscator.java | 12 +-
.../org/apache/cassandra/tools/HashPassword.java | 195 +++++++++++++++++++++
.../cassandra/audit/AuditLoggerAuthTest.java | 80 ++++++++-
.../cassandra/auth/CreateAndAlterRoleTest.java | 149 ++++++++++++++++
.../org/apache/cassandra/auth/RoleOptionsTest.java | 15 +-
.../cassandra/cql3/PasswordObfuscatorTest.java | 175 +++++++++++++++---
.../apache/cassandra/tools/HashPasswordTest.java | 150 ++++++++++++++++
.../org/apache/cassandra/tools/ToolRunner.java | 12 +-
tools/bin/hash_password | 53 ++++++
17 files changed, 882 insertions(+), 54 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index db8d987..4f7d4e7 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
4.1
+ * Pre hashed passwords in CQL (CASSANDRA-17334)
* Increase cqlsh version (CASSANDRA-17432)
* Update SUPPORTED_UPGRADE_PATHS to include 3.0 and 3.x to 4.1 paths and
remove obsolete tests (CASSANDRA-17362)
* Support DELETE in CQLSSTableWriter (CASSANDRA-14797)
diff --git a/NEWS.txt b/NEWS.txt
index 5cad3e6..cb869de 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -56,6 +56,7 @@ using the provided 'sstableupgrade' tool.
New features
------------
+ - Support for pre hashing passwords on CQL DCL commands
- Expose all client options via system_views.clients and nodetool
clientstats.
- Support for String concatenation has been added through the + operator.
- New configuration max_hints_size_per_host to limit the size of local
hints files per host in mebibytes. Setting to
diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py
index 5a9e498..762a666 100644
--- a/pylib/cqlshlib/cql3handling.py
+++ b/pylib/cqlshlib/cql3handling.py
@@ -1439,7 +1439,7 @@ syntax_rules += r'''
;
<createUserStatement> ::= "CREATE" "USER" ( "IF" "NOT" "EXISTS" )? <username>
- ( "WITH" "PASSWORD" <stringLiteral> )?
+ ( "WITH" ("HASHED")? "PASSWORD" <stringLiteral>
)?
( "SUPERUSER" | "NOSUPERUSER" )?
;
@@ -1469,7 +1469,7 @@ syntax_rules += r'''
( "WITH" <roleProperty> ("AND" <roleProperty>)*)?
;
-<roleProperty> ::= "PASSWORD" "=" <stringLiteral>
+<roleProperty> ::= (("HASHED")? "PASSWORD") "=" <stringLiteral>
| "OPTIONS" "=" <mapLiteral>
| "SUPERUSER" "=" <boolean>
| "LOGIN" "=" <boolean>
diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g
index d89097e..34c7e2e 100644
--- a/src/antlr/Lexer.g
+++ b/src/antlr/Lexer.g
@@ -150,6 +150,7 @@ K_ROLES: R O L E S;
K_SUPERUSER: S U P E R U S E R;
K_NOSUPERUSER: N O S U P E R U S E R;
K_PASSWORD: P A S S W O R D;
+K_HASHED: H A S H E D;
K_LOGIN: L O G I N;
K_NOLOGIN: N O L O G I N;
K_OPTIONS: O P T I O N S;
diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g
index efc8664..fd74c2a 100644
--- a/src/antlr/Parser.g
+++ b/src/antlr/Parser.g
@@ -1169,6 +1169,10 @@ createUserStatement returns [CreateRoleStatement stmt]
( K_WITH userPassword[opts] )?
( K_SUPERUSER { superuser = true; } | K_NOSUPERUSER { superuser = false;
} )?
{ opts.setOption(IRoleManager.Option.SUPERUSER, superuser);
+ if (opts.getPassword().isPresent() &&
opts.getHashedPassword().isPresent())
+ {
+ throw new SyntaxException("Options 'password' and 'hashed password'
are mutually exclusive");
+ }
$stmt = new CreateRoleStatement(name, opts, DCPermissions.all(),
ifNotExists); }
;
@@ -1184,7 +1188,13 @@ alterUserStatement returns [AlterRoleStatement stmt]
( K_WITH userPassword[opts] )?
( K_SUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER, true); }
| K_NOSUPERUSER { opts.setOption(IRoleManager.Option.SUPERUSER,
false); } ) ?
- { $stmt = new AlterRoleStatement(name, opts, null); }
+ {
+ if (opts.getPassword().isPresent() &&
opts.getHashedPassword().isPresent())
+ {
+ throw new SyntaxException("Options 'password' and 'hashed
password' are mutually exclusive");
+ }
+ $stmt = new AlterRoleStatement(name, opts, null);
+ }
;
/**
@@ -1232,6 +1242,10 @@ createRoleStatement returns [CreateRoleStatement stmt]
{
opts.setOption(IRoleManager.Option.SUPERUSER, false);
}
+ if (opts.getPassword().isPresent() &&
opts.getHashedPassword().isPresent())
+ {
+ throw new SyntaxException("Options 'password' and 'hashed
password' are mutually exclusive");
+ }
$stmt = new CreateRoleStatement(name, opts, dcperms.build(),
ifNotExists);
}
;
@@ -1252,7 +1266,13 @@ alterRoleStatement returns [AlterRoleStatement stmt]
}
: K_ALTER K_ROLE name=userOrRoleName
( K_WITH roleOptions[opts, dcperms] )?
- { $stmt = new AlterRoleStatement(name, opts, dcperms.isModified() ?
dcperms.build() : null); }
+ {
+ if (opts.getPassword().isPresent() &&
opts.getHashedPassword().isPresent())
+ {
+ throw new SyntaxException("Options 'password' and 'hashed
password' are mutually exclusive");
+ }
+ $stmt = new AlterRoleStatement(name, opts, dcperms.isModified() ?
dcperms.build() : null);
+ }
;
/**
@@ -1286,6 +1306,7 @@ roleOptions[RoleOptions opts, DCPermissions.Builder
dcperms]
roleOption[RoleOptions opts, DCPermissions.Builder dcperms]
: K_PASSWORD '=' v=STRING_LITERAL {
opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
+ | K_HASHED K_PASSWORD '=' v=STRING_LITERAL {
opts.setOption(IRoleManager.Option.HASHED_PASSWORD, $v.text); }
| K_OPTIONS '=' m=fullMapLiteral {
opts.setOption(IRoleManager.Option.OPTIONS, convertPropertyMap(m)); }
| K_SUPERUSER '=' b=BOOLEAN {
opts.setOption(IRoleManager.Option.SUPERUSER, Boolean.valueOf($b.text)); }
| K_LOGIN '=' b=BOOLEAN { opts.setOption(IRoleManager.Option.LOGIN,
Boolean.valueOf($b.text)); }
@@ -1300,6 +1321,7 @@ dcPermission[DCPermissions.Builder builder]
// for backwards compatibility in CREATE/ALTER USER, this has no '='
userPassword[RoleOptions opts]
: K_PASSWORD v=STRING_LITERAL {
opts.setOption(IRoleManager.Option.PASSWORD, $v.text); }
+ | K_HASHED K_PASSWORD v=STRING_LITERAL {
opts.setOption(IRoleManager.Option.HASHED_PASSWORD, $v.text); }
;
/**
@@ -1869,6 +1891,7 @@ basic_unreserved_keyword returns [String str]
| K_NOLOGIN
| K_OPTIONS
| K_PASSWORD
+ | K_HASHED
| K_EXISTS
| K_CUSTOM
| K_TRIGGER
diff --git a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index 6e8f7d8..0344de9 100644
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@ -135,10 +135,10 @@ public class CassandraRoleManager implements IRoleManager
public CassandraRoleManager()
{
supportedOptions = DatabaseDescriptor.getAuthenticator() instanceof
PasswordAuthenticator
- ? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER,
Option.PASSWORD)
+ ? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER,
Option.PASSWORD, Option.HASHED_PASSWORD)
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof
PasswordAuthenticator
- ? ImmutableSet.of(Option.PASSWORD)
+ ? ImmutableSet.of(Option.PASSWORD,
Option.HASHED_PASSWORD)
: ImmutableSet.<Option>of();
}
@@ -172,20 +172,20 @@ public class CassandraRoleManager implements IRoleManager
public void createRole(AuthenticatedUser performer, RoleResource role,
RoleOptions options)
throws RequestValidationException, RequestExecutionException
{
- String insertCql = options.getPassword().isPresent()
+ String insertCql = options.getPassword().isPresent() ||
options.getHashedPassword().isPresent()
? String.format("INSERT INTO %s.%s (role,
is_superuser, can_login, salted_hash) VALUES ('%s', %s, %s, '%s')",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES,
escape(role.getRoleName()),
- options.getSuperuser().or(false),
- options.getLogin().or(false),
-
escape(hashpw(options.getPassword().get())))
+ options.getSuperuser().orElse(false),
+ options.getLogin().orElse(false),
+
options.getHashedPassword().orElseGet(() ->
escape(hashpw(options.getPassword().get()))))
: String.format("INSERT INTO %s.%s (role,
is_superuser, can_login) VALUES ('%s', %s, %s)",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES,
escape(role.getRoleName()),
- options.getSuperuser().or(false),
- options.getLogin().or(false));
+ options.getSuperuser().orElse(false),
+ options.getLogin().orElse(false));
process(insertCql, consistencyForRoleWrite(role.getRoleName()));
}
@@ -511,6 +511,8 @@ public class CassandraRoleManager implements IRoleManager
return String.format("is_superuser =
%s", entry.getValue());
case PASSWORD:
return String.format("salted_hash =
'%s'", escape(hashpw((String) entry.getValue())));
+ case HASHED_PASSWORD:
+ return String.format("salted_hash =
'%s'", (String) entry.getValue());
default:
return null;
}
diff --git a/src/java/org/apache/cassandra/auth/IRoleManager.java
b/src/java/org/apache/cassandra/auth/IRoleManager.java
index 6a65e65..688d5bb 100644
--- a/src/java/org/apache/cassandra/auth/IRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/IRoleManager.java
@@ -41,7 +41,7 @@ public interface IRoleManager extends
AuthCache.BulkLoader<RoleResource, Set<Rol
*/
public enum Option
{
- SUPERUSER, PASSWORD, LOGIN, OPTIONS
+ SUPERUSER, PASSWORD, LOGIN, OPTIONS, HASHED_PASSWORD
}
/**
diff --git a/src/java/org/apache/cassandra/auth/RoleOptions.java
b/src/java/org/apache/cassandra/auth/RoleOptions.java
index 1205d34..c3ec56c 100644
--- a/src/java/org/apache/cassandra/auth/RoleOptions.java
+++ b/src/java/org/apache/cassandra/auth/RoleOptions.java
@@ -19,13 +19,13 @@ package org.apache.cassandra.auth;
import java.util.HashMap;
import java.util.Map;
-
-import com.google.common.base.Optional;
+import java.util.Optional;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.utils.FBUtilities;
+import org.mindrot.jbcrypt.BCrypt;
public class RoleOptions
{
@@ -68,7 +68,7 @@ public class RoleOptions
*/
public Optional<Boolean> getSuperuser()
{
- return
Optional.fromNullable((Boolean)options.get(IRoleManager.Option.SUPERUSER));
+ return Optional.ofNullable((Boolean)
options.get(IRoleManager.Option.SUPERUSER));
}
/**
@@ -77,7 +77,7 @@ public class RoleOptions
*/
public Optional<Boolean> getLogin()
{
- return
Optional.fromNullable((Boolean)options.get(IRoleManager.Option.LOGIN));
+ return Optional.ofNullable((Boolean)
options.get(IRoleManager.Option.LOGIN));
}
/**
@@ -86,7 +86,16 @@ public class RoleOptions
*/
public Optional<String> getPassword()
{
- return
Optional.fromNullable((String)options.get(IRoleManager.Option.PASSWORD));
+ return
Optional.ofNullable((String)options.get(IRoleManager.Option.PASSWORD));
+ }
+
+ /**
+ * Return the string value of the hashed password option.
+ * @return hashed password option value
+ */
+ public Optional<String> getHashedPassword()
+ {
+ return Optional.ofNullable((String)
options.get(IRoleManager.Option.HASHED_PASSWORD));
}
/**
@@ -99,7 +108,7 @@ public class RoleOptions
@SuppressWarnings("unchecked")
public Optional<Map<String, String>> getCustomOptions()
{
- return Optional.fromNullable((Map<String,
String>)options.get(IRoleManager.Option.OPTIONS));
+ return Optional.ofNullable((Map<String, String>)
options.get(IRoleManager.Option.OPTIONS));
}
/**
@@ -134,6 +143,26 @@ public class RoleOptions
throw new
InvalidRequestException(String.format("Invalid value for property '%s'. " +
"It
must be a string",
option.getKey()));
+ if
(options.containsKey(IRoleManager.Option.HASHED_PASSWORD))
+ throw new
InvalidRequestException(String.format("Properties '%s' and '%s' are mutually
exclusive",
+
IRoleManager.Option.PASSWORD, IRoleManager.Option.HASHED_PASSWORD));
+ break;
+ case HASHED_PASSWORD:
+ if (!(option.getValue() instanceof String))
+ throw new
InvalidRequestException(String.format("Invalid value for property '%s'. " +
+ "It
must be a string",
+
option.getKey()));
+ if (options.containsKey(IRoleManager.Option.PASSWORD))
+ throw new
InvalidRequestException(String.format("Properties '%s' and '%s' are mutually
exclusive",
+
IRoleManager.Option.PASSWORD, IRoleManager.Option.HASHED_PASSWORD));
+ try
+ {
+ BCrypt.checkpw("dummy", (String) option.getValue());
+ }
+ catch (Exception e)
+ {
+ throw new InvalidRequestException("Invalid hashed
password value. Please use jBcrypt.");
+ }
break;
case OPTIONS:
if (!(option.getValue() instanceof Map))
diff --git a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
index 89962f9..8e18f34 100644
--- a/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
+++ b/src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
@@ -18,7 +18,7 @@
package org.apache.cassandra.cql3;
-import com.google.common.base.Optional;
+import java.util.Optional;
import org.apache.cassandra.auth.PasswordAuthenticator;
import org.apache.cassandra.auth.RoleOptions;
@@ -63,9 +63,15 @@ public class PasswordObfuscator
Optional<String> pass = opts.getPassword();
if (!pass.isPresent() || pass.get().isEmpty())
+ pass = opts.getHashedPassword();
+ 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);
+ // Regular expression:
+ // - Match new line and case insensitive (?si), and PASSWORD_TOKEN
with greedy mode up to the start of the actual password and group it.
+ // - Quote the password between \Q and \E so any potential special
characters are ignored
+ // - Replace the match with the grouped data + the obfuscated token
+ return query.replaceAll("((?si)"+ PASSWORD_TOKEN + ".+?)\\Q" +
pass.get() + "\\E",
+ "$1" + PasswordObfuscator.OBFUSCATION_TOKEN);
}
}
diff --git a/src/java/org/apache/cassandra/tools/HashPassword.java
b/src/java/org/apache/cassandra/tools/HashPassword.java
new file mode 100644
index 0000000..c4b6314
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/HashPassword.java
@@ -0,0 +1,195 @@
+/*
+ * 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.tools;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.lang3.StringUtils;
+import org.mindrot.jbcrypt.BCrypt;
+
+public class HashPassword
+{
+ private static final String LOGROUNDS_OPTION = "logrounds";
+ private static final String HELP_OPTION = "help";
+ private static final String ENV_VAR = "environment-var";
+ private static final String PLAIN = "plain";
+ private static final String INPUT = "input";
+
+ private static final int LOGROUNDS_DEFAULT = 10;
+ private static final int MIN_PASS_LENGTH = 4;
+
+ public static void main(String[] args)
+ {
+ try
+ {
+ Options options = getOptions();
+ CommandLine cmd = parseCommandLine(args, options);
+
+ String password = null;
+ if (cmd.hasOption(ENV_VAR))
+ {
+ password = System.getenv(cmd.getOptionValue(ENV_VAR));
+ if (password == null)
+ {
+ System.err.println(String.format("Environment variable
'%s' is undefined.", cmd.getOptionValue(ENV_VAR)));
+ System.exit(1);
+ }
+ }
+ else if (cmd.hasOption(PLAIN))
+ {
+ password = cmd.getOptionValue(PLAIN);
+ }
+ else if (cmd.hasOption(INPUT))
+ {
+ String input = cmd.getOptionValue(INPUT);
+ byte[] fileInput = null;
+ if ("-".equals(input))
+ {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ int rd;
+ while ((rd = System.in.read()) != -1)
+ os.write(rd);
+ fileInput = os.toByteArray();
+ }
+ else
+ {
+ try
+ {
+ Path file = Paths.get(input);
+ fileInput = Files.readAllBytes(file);
+ }
+ catch (IOException e)
+ {
+ System.err.printf("Failed to read from '%s': %s%n",
input, e);
+ System.exit(1);
+ }
+ }
+ password = new String(fileInput, StandardCharsets.UTF_8);
+ }
+ else
+ {
+ System.err.println(String.format("One of the options --%s,
--%s or --%s must be used.",
+ ENV_VAR, PLAIN, INPUT));
+ printUsage(options);
+ System.exit(1);
+ }
+
+ if (password.chars().anyMatch(i -> i < 32))
+ System.err.println("WARNING: The provided plain text password
contains non-printable characters (ASCII<32).");
+
+ if (password.length() < MIN_PASS_LENGTH)
+ System.err.println("WARNING: The provided password is very
short, probably too short to be secure.");
+
+ int logRounds = cmd.hasOption(LOGROUNDS_OPTION) ?
Integer.parseInt(cmd.getOptionValue(LOGROUNDS_OPTION)) : LOGROUNDS_DEFAULT;
+ if (logRounds < 4 || logRounds > 30)
+ {
+ System.err.println(String.format("Bad value for --%s %d. " +
+ "Please use a value between 4
and 30 inclusively",
+ LOGROUNDS_OPTION, logRounds));
+ System.exit(1);
+ }
+
+ // The number of rounds is in fact = 2^rounds.
+ if (logRounds > 16)
+ System.err.println(String.format("WARNING: Using a high number
of hash rounds, as configured using '--%s %d' " +
+ "will consume a lot of CPU
and likely cause timeouts. Note that the parameter defines the " +
+ "logarithmic number of
rounds: %d becomes 2^%d = %d rounds",
+ LOGROUNDS_OPTION, logRounds,
+ logRounds, logRounds, 1 << logRounds));
+
+ if (password.getBytes().length > 72)
+ System.err.println(String.format("WARNING: The provided
password has a length of %d bytes, but the underlying hash/crypt algorithm " +
+ "(bcrypt) can only compare up to 72 bytes. The
password will be accepted and work, but only compared up to 72 bytes.",
+ password.getBytes().length));
+
+ String hashed = escape(hashpw(password, logRounds));
+ System.out.print(hashed);
+ System.out.flush();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static CommandLine parseCommandLine(String[] args, Options
options) throws ParseException
+ {
+ CommandLineParser parser = new GnuParser();
+
+ CommandLine cmd = parser.parse(options, args, false);
+
+ if (cmd.hasOption(HELP_OPTION))
+ {
+ printUsage(options);
+ System.exit(0);
+ }
+ return cmd;
+ }
+
+ private static Options getOptions()
+ {
+ Options options = new Options();
+ options.addOption("h", HELP_OPTION, false, "Display this help
message");
+ options.addOption("r", LOGROUNDS_OPTION, true, "Number of hash rounds
(default: " + LOGROUNDS_DEFAULT + ").");
+ OptionGroup group = new OptionGroup();
+ group.addOption(new Option("e", ENV_VAR, true,
+ "Use value of the specified environment
variable as the password"));
+ group.addOption(new Option("p", PLAIN, true,
+ "Argument is the plain text password"));
+ group.addOption(new Option("i", INPUT, true,
+ "Input is a file (or - for stdin) to read
the password from. " +
+ "Make sure that the whole input including
newlines is considered. " +
+ "For example, the shell command 'echo -n
foobar | hash_password -i -' will " +
+ "work as intended and just hash
'foobar'."));
+ options.addOptionGroup(group);
+ return options;
+ }
+
+ private static String hashpw(String password, int rounds)
+ {
+ return BCrypt.hashpw(password, BCrypt.gensalt(rounds));
+ }
+
+ private static String escape(String name)
+ {
+ return StringUtils.replace(name, "'", "''");
+ }
+
+ public static void printUsage(Options options)
+ {
+ String usage = "hash_password [options]";
+ String header = "--\n" +
+ "Hashes a plain text password and prints the hashed
password.\n" +
+ "Options are:";
+ new HelpFormatter().printHelp(usage, header, options, "");
+ }
+}
diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
index c4be4fb..71a88e5 100644
--- a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
+++ b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
@@ -61,6 +61,7 @@ public class AuditLoggerAuthTest
private static final String TEST_USER = "testuser";
private static final String TEST_ROLE = "testrole";
private static final String TEST_PW = "testpassword";
+ private static final String TEST_PW_HASH =
"$2a$10$1fI9MDCe13ZmEYW4XXZibuASNKyqOY828ELGUtml/t.0Mk/6Kqnsq";
private static final String CASS_USER = "cassandra";
private static final String CASS_PW = "cassandra";
@@ -144,6 +145,21 @@ public class AuditLoggerAuthTest
}
@Test
+ public void testCqlCreateRoleSyntaxErrorWithHashedPwd()
+ {
+ String createTestRoleCQL = String.format("CREATE ROLE %s WITH LOGIN =
%s ANDSUPERUSER = %s AND HASHED PASSWORD",
+ TEST_ROLE, true, false) +
TEST_PW_HASH;
+ String createTestRoleCQLExpected = String.format("CREATE ROLE %s WITH
LOGIN = %s ANDSUPERUSER = %s AND HASHED PASSWORD ",
+ TEST_ROLE, true,
false) + PasswordObfuscator.OBFUSCATION_TOKEN;
+
+ executeWithCredentials(Collections.singletonList(createTestRoleCQL),
CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
+ assertTrue(getInMemAuditLogger().size() > 0);
+ AuditLogEntry logEntry = getInMemAuditLogger().poll();
+ assertLogEntry(logEntry, AuditLogEntryType.REQUEST_FAILURE,
createTestRoleCQLExpected, CASS_USER, TEST_PW);
+ assertEquals(0, getInMemAuditLogger().size());
+ }
+
+ @Test
public void testCqlALTERRoleAuditing()
{
createTestRole();
@@ -151,7 +167,28 @@ 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, "foo_bar");
+ assertLogEntry(logEntry,
+ AuditLogEntryType.ALTER_ROLE,
+ "ALTER ROLE " + TEST_ROLE + " WITH PASSWORD = '" +
PasswordObfuscator.OBFUSCATION_TOKEN + "'",
+ CASS_USER,
+ "foo_bar");
+ assertEquals(0, getInMemAuditLogger().size());
+ }
+
+ @Test
+ public void testCqlALTERRoleAuditingWithHashedPwd()
+ {
+ createTestRole();
+ String cql = "ALTER ROLE " + TEST_ROLE + " WITH HASHED PASSWORD = '" +
TEST_PW_HASH + "'";
+ 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 HASHED PASSWORD = '" +
PasswordObfuscator.OBFUSCATION_TOKEN + "'",
+ CASS_USER,
+ TEST_PW_HASH);
assertEquals(0, getInMemAuditLogger().size());
}
@@ -260,6 +297,45 @@ public class AuditLoggerAuthTest
TEST_PW);
}
+ @Test
+ public void testCqlUSERCommandsAuditingWithHashedPwd()
+ {
+ //CREATE USER and ALTER USER are supported only for backwards
compatibility.
+
+ String user = TEST_ROLE + "userHasedPwd";
+ String cql = "CREATE USER " + user + " WITH HASHED PASSWORD '" +
TEST_PW_HASH + "'";
+ 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 HASHED PASSWORD '" +
PasswordObfuscator.OBFUSCATION_TOKEN + "'",
+ CASS_USER,
+ TEST_PW_HASH);
+
+ cql = "ALTER USER " + user + " WITH HASHED PASSWORD '" + TEST_PW_HASH
+ "'";
+ 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 HASHED PASSWORD '" +
PasswordObfuscator.OBFUSCATION_TOKEN + "'",
+ CASS_USER,
+ TEST_PW_HASH);
+
+ cql = "ALTER USER " + user + " WITH HASHED PASSWORD " + TEST_PW_HASH;
+ 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 HASHED PASSWORD " +
PasswordObfuscator.OBFUSCATION_TOKEN
+ + "; Syntax Exception. Obscured for security reasons.",
+ CASS_USER,
+ TEST_PW_HASH);
+ }
+
/**
* Helper methods
*/
@@ -353,4 +429,4 @@ public class AuditLoggerAuthTest
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/auth/CreateAndAlterRoleTest.java
b/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java
new file mode 100644
index 0000000..33ae129
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.auth;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.datastax.driver.core.exceptions.AuthenticationException;
+import org.apache.cassandra.Util;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertTrue;
+import static org.mindrot.jbcrypt.BCrypt.gensalt;
+import static org.mindrot.jbcrypt.BCrypt.hashpw;
+
+public class CreateAndAlterRoleTest extends CQLTester
+{
+ @BeforeClass
+ public static void setUpClass()
+ {
+ CQLTester.setUpClass();
+ requireAuthentication();
+ requireNetwork();
+ }
+
+ @Test
+ public void createAlterRoleWithHashedPassword() throws Throwable
+ {
+ String user1 = "hashed_pw_role";
+ String user2 = "pw_role";
+ String plainTextPwd = "super_secret_thing";
+ String plainTextPwd2 = "much_safer_password";
+ String hashedPassword = hashpw(plainTextPwd, gensalt(4));
+ String hashedPassword2 = hashpw(plainTextPwd2, gensalt(4));
+
+ useSuperUser();
+
+ assertInvalidMessage("Invalid hashed password value",
+ String.format("CREATE ROLE %s WITH login=true AND
hashed password='%s'",
+ user1, "this_is_an_invalid_hash"));
+ assertInvalidMessage("Options 'password' and 'hashed password' are
mutually exclusive",
+ String.format("CREATE ROLE %s WITH login=true AND
password='%s' AND hashed password='%s'",
+ user1, plainTextPwd,
hashedPassword));
+ executeNet(String.format("CREATE ROLE %s WITH login=true AND hashed
password='%s'", user1, hashedPassword));
+ executeNet(String.format("CREATE ROLE %s WITH login=true AND
password='%s'", user2, plainTextPwd));
+
+ useUser(user1, plainTextPwd);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+
+ useUser(user2, plainTextPwd);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+
+ useSuperUser();
+
+ assertInvalidMessage("Options 'password' and 'hashed password' are
mutually exclusive",
+ String.format("ALTER ROLE %s WITH password='%s'
AND hashed password='%s'",
+ user1, plainTextPwd2,
hashedPassword2));
+ executeNet(String.format("ALTER ROLE %s WITH password='%s'", user1,
plainTextPwd2));
+ executeNet(String.format("ALTER ROLE %s WITH hashed password='%s'",
user2, hashedPassword2));
+
+ useUser(user1, plainTextPwd2);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+
+ useUser(user2, plainTextPwd2);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+ }
+
+ @Test
+ public void createAlterUserWithHashedPassword() throws Throwable
+ {
+ String user1 = "hashed_pw_user";
+ String user2 = "pw_user";
+ String plainTextPwd = "super_secret_thing";
+ String plainTextPwd2 = "much_safer_password";
+ String hashedPassword = hashpw(plainTextPwd, gensalt(4));
+ String hashedPassword2 = hashpw(plainTextPwd2, gensalt(4));
+
+ useSuperUser();
+
+ assertInvalidMessage("Invalid hashed password value",
+ String.format("CREATE USER %s WITH hashed
password '%s'",
+ user1, "this_is_an_invalid_hash"));
+ executeNet(String.format("CREATE USER %s WITH hashed password '%s'",
user1, hashedPassword));
+ executeNet(String.format("CREATE USER %s WITH password '%s'", user2,
plainTextPwd));
+
+ useUser(user1, plainTextPwd);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+
+ useUser(user2, plainTextPwd);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+
+ useSuperUser();
+
+ executeNet(String.format("ALTER USER %s WITH password '%s'", user1,
plainTextPwd2));
+ executeNet(String.format("ALTER USER %s WITH hashed password '%s'",
user2, hashedPassword2));
+
+ useUser(user1, plainTextPwd2);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+
+ useUser(user2, plainTextPwd2);
+
+ executeNetWithAuthSpin("SELECT key FROM system.local");
+ }
+
+ /**
+ * Altering or creating auth may take some time to be effective
+ *
+ * @param query
+ */
+ void executeNetWithAuthSpin(String query)
+ {
+ Util.spinAssertEquals(true, () -> {
+ try
+ {
+ executeNet(query);
+ return true;
+ }
+ catch (Throwable e)
+ {
+ assertTrue("Unexpected exception: " + e, e instanceof
AuthenticationException);
+ reinitializeNetwork();
+ return false;
+ }
+ }, 10);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java
b/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java
index 6dea2b5..8a224eb 100644
--- a/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java
+++ b/test/unit/org/apache/cassandra/auth/RoleOptionsTest.java
@@ -52,10 +52,23 @@ public class RoleOptionsTest
assertInvalidOptions(opts, "Invalid value for property 'SUPERUSER'. It
must be a boolean");
opts = new RoleOptions();
+ opts.setOption(IRoleManager.Option.HASHED_PASSWORD, 99);
+ assertInvalidOptions(opts, "Invalid value for property
'HASHED_PASSWORD'. It must be a string");
+
+ opts = new RoleOptions();
+ opts.setOption(IRoleManager.Option.HASHED_PASSWORD, "invalid_hash");
+ assertInvalidOptions(opts, "Invalid hashed password value. Please use
jBcrypt.");
+
+ opts = new RoleOptions();
opts.setOption(IRoleManager.Option.OPTIONS, false);
assertInvalidOptions(opts, "Invalid value for property 'OPTIONS'. It
must be a map");
opts = new RoleOptions();
+ opts.setOption(IRoleManager.Option.PASSWORD, "abc");
+ opts.setOption(IRoleManager.Option.HASHED_PASSWORD,
"$2a$10$JSJEMFm6GeaW9XxT5JIheuEtPvat6i7uKbnTcxX3c1wshIIsGyUtG");
+ assertInvalidOptions(opts, "Properties 'PASSWORD' and
'HASHED_PASSWORD' are mutually exclusive");
+
+ opts = new RoleOptions();
opts.setOption(IRoleManager.Option.LOGIN, true);
opts.setOption(IRoleManager.Option.SUPERUSER, false);
opts.setOption(IRoleManager.Option.PASSWORD, "test");
@@ -111,7 +124,7 @@ public class RoleOptionsTest
}
catch (InvalidRequestException e)
{
- assertTrue(e.getMessage().equals(message));
+ assertEquals(message, e.getMessage());
}
}
diff --git a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
index 09a366e..96f97b6 100644
--- a/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
+++ b/test/unit/org/apache/cassandra/cql3/PasswordObfuscatorTest.java
@@ -29,13 +29,23 @@ import static org.junit.Assert.assertEquals;
public class PasswordObfuscatorTest
{
- private static final RoleOptions opts = new RoleOptions();
- private static final String optsPassword = "testpassword";
+ private static final RoleOptions plainPwdOpts = new RoleOptions();
+ private static final RoleOptions hashedPwdOpts = new RoleOptions();
+ private static final String plainPwd = "testpassword";
+ private static final String hashedPwd =
"$2a$10$1fI9MDCe13ZmEYW4XXZibuASNKyqOY828ELGUtml/t.0Mk/6Kqnsq";
@BeforeClass
public static void startup()
{
- opts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD,
"testpassword");
+
plainPwdOpts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD,
plainPwd);
+
hashedPwdOpts.setOption(org.apache.cassandra.auth.IRoleManager.Option.HASHED_PASSWORD,
hashedPwd);
+ }
+
+ @Test
+ public void testSpecialCharsObfuscation()
+ {
+ assertEquals("ALTER ROLE testrole WITH HASHED PASSWORD = '" +
OBFUSCATION_TOKEN + "'",
+ obfuscate(format("ALTER ROLE testrole WITH HASHED
PASSWORD = '%s'",hashedPwd), hashedPwdOpts));
}
@Test
@@ -45,7 +55,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("CREATE ROLE role1 WITH LOGIN = true AND
PASSWORD = '%s'", plainPwd), plainPwdOpts));
+
+ assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND HASHED
PASSWORD %s", OBFUSCATION_TOKEN),
+ obfuscate("CREATE ROLE role1 WITH LOGIN = true AND HASHED
PASSWORD = '" + hashedPwd + "'"));
+
+ assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND HASHED
PASSWORD = '%s'", OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE ROLE role1 WITH LOGIN = true AND
HASHED PASSWORD = '%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -55,14 +71,20 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("CREATE ROLE role1 WITH password = '%s'
AND LOGIN = true", plainPwd), plainPwdOpts));
+
+ assertEquals(format("CREATE ROLE role1 WITH HASHED password %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE ROLE role1 WITH HASHED password
= '%s' AND LOGIN = true", hashedPwd)));
+
+ assertEquals(format("CREATE ROLE role1 WITH HASHED password = '%s' AND
LOGIN = true", OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE ROLE role1 WITH HASHED password
= '%s' AND LOGIN = true", hashedPwd), hashedPwdOpts));
}
@Test
public void testCreateRoleWithoutPassword()
{
assertEquals("CREATE ROLE role1", obfuscate("CREATE ROLE role1"));
- assertEquals("CREATE ROLE role1", obfuscate("CREATE ROLE role1",
opts));
+ assertEquals("CREATE ROLE role1", obfuscate("CREATE ROLE role1",
plainPwdOpts));
}
@Test
@@ -70,13 +92,21 @@ public class PasswordObfuscatorTest
{
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'"));
+ "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'", plainPwd, plainPwd), plainPwdOpts));
- 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));
+ assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND HASHED
PASSWORD %s", OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE ROLE role1 WITH LOGIN = true AND
HASHED PASSWORD = '%s';" +
+ "CREATE ROLE role2 WITH LOGIN = true AND HASHED
PASSWORD = '%s'", hashedPwd, hashedPwd)));
+
+ assertEquals(format("CREATE ROLE role1 WITH LOGIN = true AND HASHED
PASSWORD = '%s';" +
+ "CREATE ROLE role2 WITH LOGIN = true AND HASHED
PASSWORD = '%s'", OBFUSCATION_TOKEN, OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE ROLE role1 WITH LOGIN = true AND
HASHED PASSWORD = '%s';" +
+ "CREATE ROLE role2 WITH LOGIN = true AND
HASHED PASSWORD = '%s'", hashedPwd, hashedPwd), hashedPwdOpts));
}
@Test
@@ -86,7 +116,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("ALTER ROLE role1 with PASSWORD = '%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("ALTER ROLE role1 with HASHED PASSWORD %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER ROLE role1 with HASHED PASSWORD =
'%s'", hashedPwd)));
+
+ assertEquals(format("ALTER ROLE role1 with HASHED PASSWORD = '%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER ROLE role1 with HASHED PASSWORD =
'%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -96,7 +132,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("ALTER ROLE role1 with PASSWORD='%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("ALTER ROLE role1 with HASHED PASSWORD %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER ROLE role1 with HASHED
PASSWORD='%s'", hashedPwd)));
+
+ assertEquals(format("ALTER ROLE role1 with HASHED PASSWORD='%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER ROLE role1 with HASHED
PASSWORD='%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -106,7 +148,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("ALTER ROLE role1 with PASSWORD= '%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("ALTER ROLE role1 with HASHED PASSWORD %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER ROLE role1 with HASHED PASSWORD=
'%s'", hashedPwd)));
+
+ assertEquals(format("ALTER ROLE role1 with HASHED PASSWORD= '%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER ROLE role1 with HASHED PASSWORD=
'%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -114,7 +162,9 @@ public class PasswordObfuscatorTest
{
assertEquals("ALTER ROLE role1", obfuscate("ALTER ROLE role1"));
- assertEquals("ALTER ROLE role1", obfuscate("ALTER ROLE role1", opts));
+ assertEquals("ALTER ROLE role1", obfuscate("ALTER ROLE role1",
plainPwdOpts));
+
+ assertEquals("ALTER ROLE role1", obfuscate("ALTER ROLE role1",
hashedPwdOpts));
}
@Test
@@ -124,7 +174,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("CREATE USER user1 with PASSWORD '%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("CREATE USER user1 with HASHED PASSWORD %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE USER user1 with HASHED PASSWORD
'%s'", hashedPwd)));
+
+ assertEquals(format("CREATE USER user1 with HASHED PASSWORD '%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE USER user1 with HASHED PASSWORD
'%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -132,7 +188,9 @@ public class PasswordObfuscatorTest
{
assertEquals("CREATE USER user1", obfuscate("CREATE USER user1"));
- assertEquals("CREATE USER user1", obfuscate("CREATE USER user1",
opts));
+ assertEquals("CREATE USER user1", obfuscate("CREATE USER user1",
plainPwdOpts));
+
+ assertEquals("CREATE USER user1", obfuscate("CREATE USER user1",
hashedPwdOpts));
}
@Test
@@ -142,7 +200,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("ALTER USER user1 with PASSWORD '%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("ALTER USER user1 with HASHED PASSWORD %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER USER user1 with HASHED PASSWORD
'%s'", hashedPwd)));
+
+ assertEquals(format("ALTER USER user1 with HASHED PASSWORD '%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER USER user1 with HASHED PASSWORD
'%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -152,7 +216,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("ALTER USER user1 with paSSwoRd '%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("ALTER USER user1 with HASHED paSSwoRd %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER USER user1 with HASHED paSSwoRd
'%s'", hashedPwd)));
+
+ assertEquals(format("ALTER USER user1 with HASHED paSSwoRd '%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER USER user1 with HASHED paSSwoRd
'%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -162,7 +232,13 @@ public class PasswordObfuscatorTest
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));
+ obfuscate(format("ALTER USER user1 with PASSWORD\n'%s'",
plainPwd), plainPwdOpts));
+
+ assertEquals(format("ALTER USER user1 with HASHED PASSWORD %s",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER USER user1 with HASHED
PASSWORD\n'%s'", hashedPwd)));
+
+ assertEquals(format("ALTER USER user1 with HASHED PASSWORD\n'%s'",
OBFUSCATION_TOKEN),
+ obfuscate(format("ALTER USER user1 with HASHED
PASSWORD\n'%s'", hashedPwd), hashedPwdOpts));
}
@Test
@@ -175,6 +251,12 @@ public class PasswordObfuscatorTest
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));
+
+ assertEquals(String.format("CREATE USER user1 with HASHED PASSWORD
%s", OBFUSCATION_TOKEN),
+ obfuscate("CREATE USER user1 with HASHED PASSWORD
'a\nb'"));
+
+ assertEquals(String.format("CREATE USER user1 with HASHED PASSWORD
'%s'", OBFUSCATION_TOKEN),
+ obfuscate(format("CREATE USER user1 with HASHED PASSWORD
'%s'", "test\npassword"), newLinePassOpts));
}
@Test
@@ -187,6 +269,12 @@ public class PasswordObfuscatorTest
emptyPassOpts.setOption(org.apache.cassandra.auth.IRoleManager.Option.PASSWORD,
"");
assertEquals("CREATE USER user1 with PASSWORD ''",
obfuscate("CREATE USER user1 with PASSWORD ''",
emptyPassOpts));
+
+ assertEquals(String.format("CREATE USER user1 with HASHED PASSWORD
%s", OBFUSCATION_TOKEN),
+ obfuscate("CREATE USER user1 with HASHED PASSWORD ''"));
+
+ assertEquals("CREATE USER user1 with HASHED PASSWORD ''",
+ obfuscate("CREATE USER user1 with HASHED PASSWORD ''",
emptyPassOpts));
}
@Test
@@ -194,6 +282,9 @@ public class PasswordObfuscatorTest
{
assertEquals(String.format("CREATE USER user1 with PASSWORD %s",
OBFUSCATION_TOKEN),
obfuscate("CREATE USER user1 with PASSWORD 'p a ss wor
d'"));
+
+ assertEquals(String.format("CREATE USER user1 with HASHED PASSWORD
%s", OBFUSCATION_TOKEN),
+ obfuscate("CREATE USER user1 with HASHED PASSWORD 'p a ss
wor d'"));
}
@Test
@@ -211,8 +302,23 @@ public class PasswordObfuscatorTest
"APPLY BATCH;", OBFUSCATION_TOKEN),
obfuscate(format("BEGIN BATCH \n" +
" CREATE ROLE alice1 WITH PASSWORD =
'%s' and LOGIN = true; \n" +
- "APPLY BATCH;", optsPassword),
- opts));
+ "APPLY BATCH;", plainPwd),
+ plainPwdOpts));
+
+ assertEquals(format("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD %s",
+ OBFUSCATION_TOKEN),
+ obfuscate("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD =
'" + hashedPwd + "' and LOGIN = true; \n" +
+ "APPLY BATCH;"));
+
+ assertEquals(format("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD =
'%s' and LOGIN = true; \n" +
+ "APPLY BATCH;", OBFUSCATION_TOKEN),
+ obfuscate(format("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD = '%s'
and LOGIN = true; \n" +
+ "APPLY BATCH;", hashedPwd),
+ hashedPwdOpts));
}
@Test
@@ -234,7 +340,26 @@ public class PasswordObfuscatorTest
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));
+ "APPLY BATCH;", plainPwd, plainPwd),
+ plainPwdOpts));
+
+ assertEquals(format("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD %s",
+ OBFUSCATION_TOKEN),
+ obfuscate("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD =
'" + hashedPwd + "' and LOGIN = true; \n" +
+ " CREATE ROLE alice2 WITH HASHED PASSWORD =
'" + hashedPwd + "' and LOGIN = true; \n" +
+ "APPLY BATCH;"));
+
+ assertEquals(format("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED PASSWORD =
'%s' and LOGIN = true; \n" +
+ " CREATE ROLE alice2 WITH HASHED PASSWORD =
'%s' and LOGIN = true; \n" +
+ "APPLY BATCH;"
+ , OBFUSCATION_TOKEN, OBFUSCATION_TOKEN),
+ obfuscate(format("BEGIN BATCH \n" +
+ " CREATE ROLE alice1 WITH HASHED
PASSWORD = '%s' and LOGIN = true; \n" +
+ " CREATE ROLE alice2 WITH HASHED
PASSWORD = '%s' and LOGIN = true; \n" +
+ "APPLY BATCH;", hashedPwd, hashedPwd),
+ hashedPwdOpts));
}
}
diff --git a/test/unit/org/apache/cassandra/tools/HashPasswordTest.java
b/test/unit/org/apache/cassandra/tools/HashPasswordTest.java
new file mode 100644
index 0000000..f84bd8b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/HashPasswordTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.tools;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner.ToolResult;
+import org.mindrot.jbcrypt.BCrypt;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class HashPasswordTest extends CQLTester
+{
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private static final String plaintextPassword = "foobar";
+ private static final String hashPasswordTool = "tools/bin/hash_password";
+
+ /* If help changed you also need to change the docs*/
+ @Test
+ public void testHelpAndShouldChangeDocs()
+ {
+ ToolResult tool = ToolRunner.invoke(hashPasswordTool, "-h");
+ tool.assertOnCleanExit();
+ String help = "usage: hash_password [options]\n" +
+ "--\n" +
+ "Hashes a plain text password and prints the hashed
password.\n" +
+ "Options are:\n" +
+ " -e,--environment-var <arg> Use value of the
specified environment\n" +
+ " variable as the
password\n" +
+ " -h,--help Display this help
message\n" +
+ " -i,--input <arg> Input is a file (or -
for stdin) to read the\n" +
+ " password from. Make sure
that the whole input including newlines is\n" +
+ " considered. For example,
the shell command 'echo -n foobar | hash_password\n" +
+ " -i -' will work as
intended and just hash 'foobar'.\n" +
+ " -p,--plain <arg> Argument is the plain
text password\n" +
+ " -r,--logrounds <arg> Number of hash rounds
(default: 10).\n";
+ assertEquals(help, tool.getStdout());
+ }
+
+ @Test
+ public void testPlain()
+ {
+ ToolResult tool = ToolRunner.invoke(hashPasswordTool, "--plain",
plaintextPassword);
+ tool.assertOnCleanExit();
+ String hashed = tool.getStdout();
+ assertTrue("Hashed password does not validate: " + hashed,
BCrypt.checkpw(plaintextPassword, hashed));
+ }
+
+ @Test
+ public void testStdIn()
+ {
+ ToolResult tool = ToolRunner.invoke(Collections.emptyMap(),
+ new
ByteArrayInputStream(plaintextPassword.getBytes()),
+ Arrays.asList(hashPasswordTool,
"--input", "-"));
+ tool.assertOnCleanExit();
+ String hashed = tool.getStdout();
+ assertTrue("Hashed password does not validate: " + hashed,
BCrypt.checkpw(plaintextPassword, hashed));
+ }
+
+ @Test
+ public void testFile() throws IOException
+ {
+ File file = temporaryFolder.newFile();
+ Files.write(file.toPath(), plaintextPassword.getBytes());
+
+ ToolResult tool = ToolRunner.invoke(hashPasswordTool, "--input",
file.getAbsolutePath());
+ tool.assertOnCleanExit();
+ String hashed = tool.getStdout();
+ assertTrue("Hashed password does not validate: " + hashed,
BCrypt.checkpw(plaintextPassword, hashed));
+ }
+
+ @Test
+ public void testEnvVar()
+ {
+ ToolResult tool =
ToolRunner.invoke(Collections.singletonMap("THE_PASSWORD", plaintextPassword),
+ null,
+ Arrays.asList(hashPasswordTool,
"--environment-var", "THE_PASSWORD"));
+ tool.assertOnCleanExit();
+ String hashed = tool.getStdout();
+ assertTrue("Hashed password does not validate: " + hashed,
BCrypt.checkpw(plaintextPassword, hashed));
+ }
+
+ @Test
+ public void testLogRounds()
+ {
+ ToolResult tool = ToolRunner.invoke(hashPasswordTool, "--plain",
plaintextPassword, "-r", "10");
+ tool.assertOnCleanExit();
+ String hashed = tool.getStdout();
+ assertTrue("Hashed password does not validate: " + hashed,
BCrypt.checkpw(plaintextPassword, hashed));
+ }
+
+ @Test
+ public void testShortPass()
+ {
+ ToolResult tool = ToolRunner.invoke(hashPasswordTool, "--plain", "A");
+ tool.assertOnExitCode();
+ assertThat(tool.getStderr(), containsString("password is very short"));
+ }
+
+ @Test
+ public void testErrorMessages()
+ {
+ assertToolError("One of the options --environment-var, --plain or
--input must be used.", hashPasswordTool);
+ assertToolError("Environment variable
'non_existing_environment_variable_name' is undefined.",
+ hashPasswordTool,
+ "--environment-var",
+ "non_existing_environment_variable_name");
+ assertToolError("Failed to read from '/foo/bar/baz/blah/yadda': ",
+ hashPasswordTool,
+ "--input",
+ "/foo/bar/baz/blah/yadda");
+ }
+
+ private static void assertToolError(String expectedMessage, String... args)
+ {
+ ToolResult tool = ToolRunner.invoke(args);
+ assertEquals(1, tool.getExitCode());
+ assertThat(tool.getStderr(), containsString(expectedMessage));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java
b/test/unit/org/apache/cassandra/tools/ToolRunner.java
index 0ad88ef..f650f34 100644
--- a/test/unit/org/apache/cassandra/tools/ToolRunner.java
+++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java
@@ -119,11 +119,13 @@ public class ToolRunner
private final InputStream input;
private final T out;
+ private final boolean autoCloseOut;
- private StreamGobbler(InputStream input, T out)
+ private StreamGobbler(InputStream input, T out, boolean autoCloseOut)
{
this.input = input;
this.out = out;
+ this.autoCloseOut = autoCloseOut;
}
public void run()
@@ -136,6 +138,8 @@ public class ToolRunner
int read = input.read(buffer);
if (read == -1)
{
+ if (autoCloseOut)
+ out.close();
return;
}
out.write(buffer, 0, read);
@@ -536,19 +540,19 @@ public class ToolRunner
if (includeStdinWatcher)
numWatchers = 3;
ioWatchers = new Thread[numWatchers];
- ioWatchers[0] = new Thread(new
StreamGobbler<>(process.getErrorStream(), err));
+ ioWatchers[0] = new Thread(new
StreamGobbler<>(process.getErrorStream(), err, false));
ioWatchers[0].setDaemon(true);
ioWatchers[0].setName("IO Watcher stderr");
ioWatchers[0].start();
- ioWatchers[1] = new Thread(new
StreamGobbler<>(process.getInputStream(), out));
+ ioWatchers[1] = new Thread(new
StreamGobbler<>(process.getInputStream(), out, false));
ioWatchers[1].setDaemon(true);
ioWatchers[1].setName("IO Watcher stdout");
ioWatchers[1].start();
if (includeStdinWatcher)
{
- ioWatchers[2] = new Thread(new StreamGobbler<>(stdin,
process.getOutputStream()));
+ ioWatchers[2] = new Thread(new StreamGobbler<>(stdin,
process.getOutputStream(), true));
ioWatchers[2].setDaemon(true);
ioWatchers[2].setName("IO Watcher stdin");
ioWatchers[2].start();
diff --git a/tools/bin/hash_password b/tools/bin/hash_password
new file mode 100755
index 0000000..dc48ce1
--- /dev/null
+++ b/tools/bin/hash_password
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# 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.
+
+if [ "x$CASSANDRA_INCLUDE" = "x" ]; then
+ # Locations (in order) to use when searching for an include file.
+ for include in "`dirname "$0"`/cassandra.in.sh" \
+ "$HOME/.cassandra.in.sh" \
+ /usr/share/cassandra/cassandra.in.sh \
+ /usr/local/share/cassandra/cassandra.in.sh \
+ /opt/cassandra/cassandra.in.sh; do
+ if [ -r "$include" ]; then
+ . "$include"
+ break
+ fi
+ done
+elif [ -r "$CASSANDRA_INCLUDE" ]; then
+ . "$CASSANDRA_INCLUDE"
+fi
+
+if [ -z "$CLASSPATH" ]; then
+ echo "You must set the CLASSPATH var" >&2
+ exit 1
+fi
+
+if [ "x${MAX_HEAP_SIZE}" = "x" ]; then
+ MAX_HEAP_SIZE="256M"
+fi
+
+if [ "x${MAX_DIRECT_MEMORY}" = "x" ]; then
+ MAX_DIRECT_MEMORY="2G"
+fi
+
+JVM_OPTS="${JVM_OPTS} -Xmx${MAX_HEAP_SIZE}
-XX:MaxDirectMemorySize=${MAX_DIRECT_MEMORY}"
+
+"${JAVA}" ${JAVA_AGENT} -ea -cp "${CLASSPATH}" ${JVM_OPTS} \
+ -Dcassandra.storagedir="${cassandra_storagedir}" \
+ -Dlogback.configurationFile=logback-tools.xml \
+ org.apache.cassandra.tools.HashPassword "$@"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]