Pre hashed passwords in CQL

patch by Robert Stupp and Berenguer Blasi; reviewed by Andres de la Peña for CASSANDRA-17334

Co-authored-by: Robert Stupp <snazy@snazy.de>
Co-authored-by: Berenguer Blasi <berenguerblasi@gmail.com>
This commit is contained in:
Bereng 2022-03-01 07:44:19 +01:00
parent 94580a272e
commit 30ad754d7e
19 changed files with 884 additions and 54 deletions

View File

@ -2,6 +2,7 @@
* Log largest memtable flush at info instead of debug (CASSANDRA-17472)
* Add native transport rate limiter options to example cassandra.yaml, and expose metric for dispatch rate (CASSANDRA-17423)
* Add diagnostic events for guardrails (CASSANDRA-17197)
* 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)

View File

@ -58,6 +58,7 @@ New features
------------
- Support for native transport rate limiting via native_transport_rate_limiting_enabled and
native_transport_max_requests_per_second in cassandra.yaml.
- 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

View File

@ -24,6 +24,7 @@ tools/bin/cassandra-stress usr/bin
tools/bin/fqltool usr/bin
tools/bin/auditlogviewer usr/bin
tools/bin/jmxtool usr/bin
tools/bin/hash_password usr/bin
lib/*.jar usr/share/cassandra/lib
lib/*.zip usr/share/cassandra/lib
lib/sigar-bin/* usr/share/cassandra/lib/sigar-bin

View File

@ -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>

View File

@ -202,6 +202,7 @@ This package contains extra tools for working with Cassandra clusters.
%attr(755,root,root) %{_bindir}/jmxtool
%attr(755,root,root) %{_bindir}/fqltool
%attr(755,root,root) %{_bindir}/generatetokens
%attr(755,root,root) %{_bindir}/hash_password
%changelog

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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
}
/**

View File

@ -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))

View File

@ -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;
@ -62,10 +62,16 @@ public class PasswordObfuscator
return query;
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);
}
}

View File

@ -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, "");
}
}

View File

@ -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";
@ -143,6 +144,21 @@ public class AuditLoggerAuthTest
assertEquals(0, getInMemAuditLogger().size());
}
@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()
{
@ -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
*/

View File

@ -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);
}
}

View File

@ -51,10 +51,23 @@ public class RoleOptionsTest
opts.setOption(IRoleManager.Option.SUPERUSER, new HashSet<>());
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);
@ -111,7 +124,7 @@ public class RoleOptionsTest
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().equals(message));
assertEquals(message, e.getMessage());
}
}

View File

@ -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
@ -72,11 +94,19 @@ public class PasswordObfuscatorTest
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));
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 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));
}
}

View File

@ -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));
}
}

View File

@ -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();

53
tools/bin/hash_password Executable file
View File

@ -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 "$@"