You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by sn...@apache.org on 2015/03/24 14:15:43 UTC
[1/3] cassandra git commit: Make PasswordAuthenticator number of
hashing rounds configurable
Repository: cassandra
Updated Branches:
refs/heads/cassandra-2.1 bcf0ec681 -> bf1ea024d
refs/heads/trunk 789aa72e5 -> 5735f2a25
Make PasswordAuthenticator number of hashing rounds configurable
Patch by Sam Tunnicliffe; Reviewed by Robert Stupp for CASSANDRA-8085
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/bf1ea024
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/bf1ea024
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/bf1ea024
Branch: refs/heads/cassandra-2.1
Commit: bf1ea024d50dc0243bb8cec2a23b01a709b4e705
Parents: bcf0ec6
Author: Sam Tunnicliffe <sa...@beobal.com>
Authored: Tue Mar 24 14:14:18 2015 +0100
Committer: Robert Stupp <sn...@snazy.de>
Committed: Tue Mar 24 14:14:18 2015 +0100
----------------------------------------------------------------------
CHANGES.txt | 1 +
.../cassandra/auth/PasswordAuthenticator.java | 16 ++++++++++++++--
2 files changed, 15 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cassandra/blob/bf1ea024/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 0e75973..e85b622 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
2.1.4
+ * Make PasswordAuthenticator number of hashing rounds configurable (CASSANDRA-8085)
* Fix AssertionError when binding nested collections in DELETE (CASSANDRA-8900)
* Check for overlap with non-early sstables in LCS (CASSANDRA-8739)
* Only calculate max purgable timestamp if we have to (CASSANDRA-8914)
http://git-wip-us.apache.org/repos/asf/cassandra/blob/bf1ea024/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
index 9570770..7d30664 100644
--- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@ -31,6 +31,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.config.Config;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.QueryOptions;
@@ -51,8 +52,19 @@ public class PasswordAuthenticator implements ISaslAwareAuthenticator
{
private static final Logger logger = LoggerFactory.getLogger(PasswordAuthenticator.class);
- // 2 ** GENSALT_LOG2_ROUNS rounds of hashing will be performed.
- private static final int GENSALT_LOG2_ROUNDS = 10;
+ // 2 ** GENSALT_LOG2_ROUNDS rounds of hashing will be performed.
+ private static final String GENSALT_LOG2_ROUNDS_PROPERTY = Config.PROPERTY_PREFIX + "auth_bcrypt_gensalt_log2_rounds";
+ private static final int GENSALT_LOG2_ROUNDS = getGensaltLogRounds();
+
+ static int getGensaltLogRounds()
+ {
+ int rounds = Integer.getInteger(GENSALT_LOG2_ROUNDS_PROPERTY, 10);
+ if (rounds < 4 || rounds > 31)
+ throw new RuntimeException(new ConfigurationException(String.format("Bad value for system property -D%s. " +
+ "Please use a value 4 and 31",
+ GENSALT_LOG2_ROUNDS_PROPERTY)));
+ return rounds;
+ }
// name of the hash column.
private static final String SALTED_HASH = "salted_hash";
[3/3] cassandra git commit: Merge branch 'cassandra-2.1' into trunk
Posted by sn...@apache.org.
Merge branch 'cassandra-2.1' into trunk
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/5735f2a2
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/5735f2a2
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/5735f2a2
Branch: refs/heads/trunk
Commit: 5735f2a2553b83f90a4e7320e82bf326d25f5d18
Parents: 789aa72 bf1ea02
Author: Sam Tunnicliffe <sa...@beobal.com>
Authored: Tue Mar 24 14:15:14 2015 +0100
Committer: Robert Stupp <sn...@snazy.de>
Committed: Tue Mar 24 14:15:14 2015 +0100
----------------------------------------------------------------------
CHANGES.txt | 1 +
.../apache/cassandra/auth/CassandraRoleManager.java | 16 ++++++++++++++--
2 files changed, 15 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cassandra/blob/5735f2a2/CHANGES.txt
----------------------------------------------------------------------
diff --cc CHANGES.txt
index d39db88,e85b622..e6020de
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@@ -1,81 -1,5 +1,82 @@@
+3.0
+ * Add a key-value payload for third party usage (CASSANDRA-8553)
+ * Bump metrics-reporter-config dependency for metrics 3.0 (CASSANDRA-8149)
+ * Partition intra-cluster message streams by size, not type (CASSANDRA-8789)
+ * Add WriteFailureException to native protocol, notify coordinator of
+ write failures (CASSANDRA-8592)
+ * Convert SequentialWriter to nio (CASSANDRA-8709)
+ * Add role based access control (CASSANDRA-7653, 8650, 7216, 8760, 8849, 8761, 8850)
+ * Record client ip address in tracing sessions (CASSANDRA-8162)
+ * Indicate partition key columns in response metadata for prepared
+ statements (CASSANDRA-7660)
+ * Merge UUIDType and TimeUUIDType parse logic (CASSANDRA-8759)
+ * Avoid memory allocation when searching index summary (CASSANDRA-8793)
+ * Optimise (Time)?UUIDType Comparisons (CASSANDRA-8730)
+ * Make CRC32Ex into a separate maven dependency (CASSANDRA-8836)
+ * Use preloaded jemalloc w/ Unsafe (CASSANDRA-8714)
+ * Avoid accessing partitioner through StorageProxy (CASSANDRA-8244, 8268)
+ * Upgrade Metrics library and remove depricated metrics (CASSANDRA-5657)
+ * Serializing Row cache alternative, fully off heap (CASSANDRA-7438)
+ * Duplicate rows returned when in clause has repeated values (CASSANDRA-6707)
+ * Make CassandraException unchecked, extend RuntimeException (CASSANDRA-8560)
+ * Support direct buffer decompression for reads (CASSANDRA-8464)
+ * DirectByteBuffer compatible LZ4 methods (CASSANDRA-7039)
+ * Group sstables for anticompaction correctly (CASSANDRA-8578)
+ * Add ReadFailureException to native protocol, respond
+ immediately when replicas encounter errors while handling
+ a read request (CASSANDRA-7886)
+ * Switch CommitLogSegment from RandomAccessFile to nio (CASSANDRA-8308)
+ * Allow mixing token and partition key restrictions (CASSANDRA-7016)
+ * Support index key/value entries on map collections (CASSANDRA-8473)
+ * Modernize schema tables (CASSANDRA-8261)
+ * Support for user-defined aggregation functions (CASSANDRA-8053)
+ * Fix NPE in SelectStatement with empty IN values (CASSANDRA-8419)
+ * Refactor SelectStatement, return IN results in natural order instead
+ of IN value list order and ignore duplicate values in partition key IN restrictions (CASSANDRA-7981)
+ * Support UDTs, tuples, and collections in user-defined
+ functions (CASSANDRA-7563)
+ * Fix aggregate fn results on empty selection, result column name,
+ and cqlsh parsing (CASSANDRA-8229)
+ * Mark sstables as repaired after full repair (CASSANDRA-7586)
+ * Extend Descriptor to include a format value and refactor reader/writer
+ APIs (CASSANDRA-7443)
+ * Integrate JMH for microbenchmarks (CASSANDRA-8151)
+ * Keep sstable levels when bootstrapping (CASSANDRA-7460)
+ * Add Sigar library and perform basic OS settings check on startup (CASSANDRA-7838)
+ * Support for aggregation functions (CASSANDRA-4914)
+ * Remove cassandra-cli (CASSANDRA-7920)
+ * Accept dollar quoted strings in CQL (CASSANDRA-7769)
+ * Make assassinate a first class command (CASSANDRA-7935)
+ * Support IN clause on any partition key column (CASSANDRA-7855)
+ * Support IN clause on any clustering column (CASSANDRA-4762)
+ * Improve compaction logging (CASSANDRA-7818)
+ * Remove YamlFileNetworkTopologySnitch (CASSANDRA-7917)
+ * Do anticompaction in groups (CASSANDRA-6851)
+ * Support user-defined functions (CASSANDRA-7395, 7526, 7562, 7740, 7781, 7929,
+ 7924, 7812, 8063, 7813, 7708)
+ * Permit configurable timestamps with cassandra-stress (CASSANDRA-7416)
+ * Move sstable RandomAccessReader to nio2, which allows using the
+ FILE_SHARE_DELETE flag on Windows (CASSANDRA-4050)
+ * Remove CQL2 (CASSANDRA-5918)
+ * Add Thrift get_multi_slice call (CASSANDRA-6757)
+ * Optimize fetching multiple cells by name (CASSANDRA-6933)
+ * Allow compilation in java 8 (CASSANDRA-7028)
+ * Make incremental repair default (CASSANDRA-7250)
+ * Enable code coverage thru JaCoCo (CASSANDRA-7226)
+ * Switch external naming of 'column families' to 'tables' (CASSANDRA-4369)
+ * Shorten SSTable path (CASSANDRA-6962)
+ * Use unsafe mutations for most unit tests (CASSANDRA-6969)
+ * Fix race condition during calculation of pending ranges (CASSANDRA-7390)
+ * Fail on very large batch sizes (CASSANDRA-8011)
+ * Improve concurrency of repair (CASSANDRA-6455, 8208)
+ * Select optimal CRC32 implementation at runtime (CASSANDRA-8614)
+ * Evaluate MurmurHash of Token once per query (CASSANDRA-7096)
+ * Generalize progress reporting (CASSANDRA-8901)
+ * Resumable bootstrap streaming (CASSANDRA-8838)
+ * Allow scrub for secondary index (CASSANDRA-5174)
+
2.1.4
+ * Make PasswordAuthenticator number of hashing rounds configurable (CASSANDRA-8085)
* Fix AssertionError when binding nested collections in DELETE (CASSANDRA-8900)
* Check for overlap with non-early sstables in LCS (CASSANDRA-8739)
* Only calculate max purgable timestamp if we have to (CASSANDRA-8914)
http://git-wip-us.apache.org/repos/asf/cassandra/blob/5735f2a2/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
----------------------------------------------------------------------
diff --cc src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index 05a4fc8,0000000..83b309d
mode 100644,000000..100644
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@@ -1,595 -1,0 +1,607 @@@
+/*
+ * 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 java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.*;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.concurrent.ScheduledExecutors;
++import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.Schema;
+import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.exceptions.*;
+import org.apache.cassandra.service.QueryState;
+import org.apache.cassandra.transport.messages.ResultMessage;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.mindrot.jbcrypt.BCrypt;
+
+/**
+ * Responsible for the creation, maintainance and delation of roles
+ * for the purposes of authentication and authorization.
+ * Role data is stored internally, using the roles and role_members tables
+ * in the system_auth keyspace.
+ *
+ * Additionally, if org.apache.cassandra.auth.PasswordAuthenticator is used,
+ * encrypted passwords are also stored in the system_auth.roles table. This
+ * coupling between the IAuthenticator and IRoleManager implementations exists
+ * because setting a role's password via CQL is done with a CREATE ROLE or
+ * ALTER ROLE statement, the processing of which is handled by IRoleManager.
+ * As IAuthenticator is concerned only with credentials checking and has no
+ * means to modify passwords, PasswordAuthenticator depends on
+ * CassandraRoleManager for those functions.
+ *
+ * Alternative IAuthenticator implementations may be used in conjunction with
+ * CassandraRoleManager, but WITH PASSWORD = 'password' will not be supported
+ * in CREATE/ALTER ROLE statements.
+ *
+ * Such a configuration could be implemented using a custom IRoleManager that
+ * extends CassandraRoleManager and which includes Option.PASSWORD in the Set<Option>
+ * returned from supportedOptions/alterableOptions. Any additional processing
+ * of the password itself (such as storing it in an alternative location) would
+ * be added in overriden createRole and alterRole implementations.
+ */
+public class CassandraRoleManager implements IRoleManager
+{
+ private static final Logger logger = LoggerFactory.getLogger(CassandraRoleManager.class);
+
+ static final String DEFAULT_SUPERUSER_NAME = "cassandra";
+ static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
+
+ // Transform a row in the AuthKeyspace.ROLES to a Role instance
+ private static final Function<UntypedResultSet.Row, Role> ROW_TO_ROLE = new Function<UntypedResultSet.Row, Role>()
+ {
+ public Role apply(UntypedResultSet.Row row)
+ {
+ return new Role(row.getString("role"),
+ row.getBoolean("is_superuser"),
+ row.getBoolean("can_login"),
+ row.has("member_of") ? row.getSet("member_of", UTF8Type.instance)
+ : Collections.<String>emptySet());
+ }
+ };
+
+ public static final String LEGACY_USERS_TABLE = "users";
+ // Transform a row in the legacy system_auth.users table to a Role instance,
+ // used to fallback to previous schema on a mixed cluster during an upgrade
+ private static final Function<UntypedResultSet.Row, Role> LEGACY_ROW_TO_ROLE = new Function<UntypedResultSet.Row, Role>()
+ {
+ public Role apply(UntypedResultSet.Row row)
+ {
+ return new Role(row.getString("name"),
+ row.getBoolean("super"),
+ true,
+ Collections.<String>emptySet());
+ }
+ };
+
- // 2 ** GENSALT_LOG2_ROUNS rounds of hashing will be performed.
- private static final int GENSALT_LOG2_ROUNDS = 10;
++ // 2 ** GENSALT_LOG2_ROUNDS rounds of hashing will be performed.
++ private static final String GENSALT_LOG2_ROUNDS_PROPERTY = Config.PROPERTY_PREFIX + "auth_bcrypt_gensalt_log2_rounds";
++ private static final int GENSALT_LOG2_ROUNDS = getGensaltLogRounds();
++
++ static int getGensaltLogRounds()
++ {
++ int rounds = Integer.getInteger(GENSALT_LOG2_ROUNDS_PROPERTY, 10);
++ if (rounds < 4 || rounds > 31)
++ throw new ConfigurationException(String.format("Bad value for system property -D%s." +
++ "Please use a value 4 and 31",
++ GENSALT_LOG2_ROUNDS_PROPERTY));
++ return rounds;
++ }
+
+ // NullObject returned when a supplied role name not found in AuthKeyspace.ROLES
+ private static final Role NULL_ROLE = new Role(null, false, false, Collections.<String>emptySet());
+
+ private SelectStatement loadRoleStatement;
+ private SelectStatement legacySelectUserStatement;
+
+ private final Set<Option> supportedOptions;
+ private final Set<Option> alterableOptions;
+
+ public CassandraRoleManager()
+ {
+ supportedOptions = DatabaseDescriptor.getAuthenticator().getClass() == PasswordAuthenticator.class
+ ? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD)
+ : ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
+ alterableOptions = DatabaseDescriptor.getAuthenticator().getClass().equals(PasswordAuthenticator.class)
+ ? ImmutableSet.of(Option.PASSWORD)
+ : ImmutableSet.<Option>of();
+ }
+
+ public void setup()
+ {
+ loadRoleStatement = (SelectStatement) prepare("SELECT * from %s.%s WHERE role = ?",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES);
+ // If the old users table exists, we may need to migrate the legacy authn
+ // data to the new table. We also need to prepare a statement to read from
+ // it, so we can continue to use the old tables while the cluster is upgraded.
+ // Otherwise, we may need to create a default superuser role to enable others
+ // to be added.
+ if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") != null)
+ {
+ legacySelectUserStatement = (SelectStatement) prepare("SELECT * FROM %s.%s WHERE name = ?",
+ AuthKeyspace.NAME,
+ LEGACY_USERS_TABLE);
+ scheduleSetupTask(new Runnable()
+ {
+ public void run()
+ {
+ convertLegacyData();
+ }
+ });
+ }
+ else
+ {
+ scheduleSetupTask(new Runnable()
+ {
+ public void run()
+ {
+ setupDefaultRole();
+ }
+ });
+ }
+ }
+
+ public Set<Option> supportedOptions()
+ {
+ return supportedOptions;
+ }
+
+ public Set<Option> alterableOptions()
+ {
+ return alterableOptions;
+ }
+
+ public void createRole(AuthenticatedUser performer, RoleResource role, RoleOptions options)
+ throws RequestValidationException, RequestExecutionException
+ {
+ String insertCql = options.getPassword().isPresent()
+ ? String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) VALUES ('%s', %s, %s, '%s')",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ escape(role.getRoleName()),
+ options.getSuperuser().get(),
+ options.getLogin().get(),
+ escape(hashpw(options.getPassword().get())))
+ : String.format("INSERT INTO %s.%s (role, is_superuser, can_login) VALUES ('%s', %s, %s)",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ escape(role.getRoleName()),
+ options.getSuperuser().get(),
+ options.getLogin().get());
+ process(insertCql, consistencyForRole(role.getRoleName()));
+ }
+
+ public void dropRole(AuthenticatedUser performer, RoleResource role) throws RequestValidationException, RequestExecutionException
+ {
+ process(String.format("DELETE FROM %s.%s WHERE role = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ escape(role.getRoleName())),
+ consistencyForRole(role.getRoleName()));
+ removeAllMembers(role.getRoleName());
+ }
+
+ public void alterRole(AuthenticatedUser performer, RoleResource role, RoleOptions options)
+ {
+ // Unlike most of the other data access methods here, this does not use a
+ // prepared statement in order to allow the set of assignments to be variable.
+ String assignments = Joiner.on(',').join(Iterables.filter(optionsToAssignments(options.getOptions()),
+ Predicates.notNull()));
+ if (!Strings.isNullOrEmpty(assignments))
+ {
+ QueryProcessor.process(String.format("UPDATE %s.%s SET %s WHERE role = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ assignments,
+ escape(role.getRoleName())),
+ consistencyForRole(role.getRoleName()));
+ }
+ }
+
+ public void grantRole(AuthenticatedUser performer, RoleResource role, RoleResource grantee)
+ throws RequestValidationException, RequestExecutionException
+ {
+ if (getRoles(grantee, true).contains(role))
+ throw new InvalidRequestException(String.format("%s is a member of %s",
+ grantee.getRoleName(),
+ role.getRoleName()));
+ if (getRoles(role, true).contains(grantee))
+ throw new InvalidRequestException(String.format("%s is a member of %s",
+ role.getRoleName(),
+ grantee.getRoleName()));
+
+ modifyRoleMembership(grantee.getRoleName(), role.getRoleName(), "+");
+ process(String.format("INSERT INTO %s.%s (role, member) values ('%s', '%s')",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLE_MEMBERS,
+ escape(role.getRoleName()),
+ escape(grantee.getRoleName())),
+ consistencyForRole(role.getRoleName()));
+ }
+
+ public void revokeRole(AuthenticatedUser performer, RoleResource role, RoleResource revokee)
+ throws RequestValidationException, RequestExecutionException
+ {
+ if (!getRoles(revokee, false).contains(role))
+ throw new InvalidRequestException(String.format("%s is not a member of %s",
+ revokee.getRoleName(),
+ role.getRoleName()));
+
+ modifyRoleMembership(revokee.getRoleName(), role.getRoleName(), "-");
+ process(String.format("DELETE FROM %s.%s WHERE role = '%s' and member = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLE_MEMBERS,
+ escape(role.getRoleName()),
+ escape(revokee.getRoleName())),
+ consistencyForRole(role.getRoleName()));
+ }
+
+ public Set<RoleResource> getRoles(RoleResource grantee, boolean includeInherited) throws RequestValidationException, RequestExecutionException
+ {
+ Set<RoleResource> roles = new HashSet<>();
+ Role role = getRole(grantee.getRoleName());
+ if (!role.equals(NULL_ROLE))
+ {
+ roles.add(RoleResource.role(role.name));
+ collectRoles(role, roles, includeInherited);
+ }
+ return roles;
+ }
+
+ public Set<RoleResource> getAllRoles() throws RequestValidationException, RequestExecutionException
+ {
+ UntypedResultSet rows = QueryProcessor.process(String.format("SELECT role from %s.%s",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES),
+ ConsistencyLevel.QUORUM);
+ Iterable<RoleResource> roles = Iterables.transform(rows, new Function<UntypedResultSet.Row, RoleResource>()
+ {
+ public RoleResource apply(UntypedResultSet.Row row)
+ {
+ return RoleResource.role(row.getString("role"));
+ }
+ });
+ return ImmutableSet.<RoleResource>builder().addAll(roles).build();
+ }
+
+ public boolean isSuper(RoleResource role)
+ {
+ return getRole(role.getRoleName()).isSuper;
+ }
+
+ public boolean canLogin(RoleResource role)
+ {
+ return getRole(role.getRoleName()).canLogin;
+ }
+
+ public Map<String, String> getCustomOptions(RoleResource role)
+ {
+ return Collections.emptyMap();
+ }
+
+ public boolean isExistingRole(RoleResource role)
+ {
+ return getRole(role.getRoleName()) != NULL_ROLE;
+ }
+
+ public Set<? extends IResource> protectedResources()
+ {
+ return ImmutableSet.of(DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLES),
+ DataResource.table(AuthKeyspace.NAME, AuthKeyspace.ROLE_MEMBERS));
+ }
+
+ public void validateConfiguration() throws ConfigurationException
+ {
+ }
+
+ /*
+ * Create the default superuser role to bootstrap role creation on a clean system. Preemptively
+ * gives the role the default password so PasswordAuthenticator can be used to log in (if
+ * configured)
+ */
+ private static void setupDefaultRole()
+ {
+ try
+ {
+ if (!hasExistingRoles())
+ {
+ QueryProcessor.process(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) " +
+ "VALUES ('%s', true, true, '%s')",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ DEFAULT_SUPERUSER_NAME,
+ escape(hashpw(DEFAULT_SUPERUSER_PASSWORD))),
+ consistencyForRole(DEFAULT_SUPERUSER_NAME));
+ logger.info("Created default superuser role '{}'", DEFAULT_SUPERUSER_NAME);
+ }
+ }
+ catch (RequestExecutionException e)
+ {
+ logger.warn("CassandraRoleManager skipped default role setup: some nodes were not ready");
+ }
+ }
+
+ private static boolean hasExistingRoles() throws RequestExecutionException
+ {
+ // Try looking up the 'cassandra' default role first, to avoid the range query if possible.
+ String defaultSUQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", AuthKeyspace.NAME, AuthKeyspace.ROLES, DEFAULT_SUPERUSER_NAME);
+ String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", AuthKeyspace.NAME, AuthKeyspace.ROLES);
+ return !process(defaultSUQuery, ConsistencyLevel.ONE).isEmpty()
+ || !process(defaultSUQuery, ConsistencyLevel.QUORUM).isEmpty()
+ || !process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
+ }
+
+ private void scheduleSetupTask(Runnable runnable)
+ {
+ // The delay is to give the node a chance to see its peers before attempting the operation
+ ScheduledExecutors.optionalTasks.schedule(runnable, AuthKeyspace.SUPERUSER_SETUP_DELAY, TimeUnit.MILLISECONDS);
+ }
+
+ /*
+ * Copy legacy auth data from the system_auth.users & system_auth.credentials tables to
+ * the new system_auth.roles table. This setup is not performed if AllowAllAuthenticator
+ * is configured (see Auth#setup).
+ */
+ private void convertLegacyData()
+ {
+ try
+ {
+ // read old data at QUORUM as it may contain the data for the default superuser
+ if (Schema.instance.getCFMetaData("system_auth", "users") != null)
+ {
+ logger.info("Converting legacy users");
+ UntypedResultSet users = QueryProcessor.process("SELECT * FROM system_auth.users",
+ ConsistencyLevel.QUORUM);
+ for (UntypedResultSet.Row row : users)
+ {
+ RoleOptions options = new RoleOptions();
+ options.setOption(Option.SUPERUSER, row.getBoolean("super"));
+ options.setOption(Option.LOGIN, true);
+ createRole(null, RoleResource.role(row.getString("name")), options);
+ }
+ logger.info("Completed conversion of legacy users");
+ }
+
+ if (Schema.instance.getCFMetaData("system_auth", "credentials") != null)
+ {
+ logger.info("Migrating legacy credentials data to new system table");
+ UntypedResultSet credentials = QueryProcessor.process("SELECT * FROM system_auth.credentials",
+ ConsistencyLevel.QUORUM);
+ for (UntypedResultSet.Row row : credentials)
+ {
+ // Write the password directly into the table to avoid doubly encrypting it
+ QueryProcessor.process(String.format("UPDATE %s.%s SET salted_hash = '%s' WHERE role = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ row.getString("salted_hash"),
+ row.getString("username")),
+ consistencyForRole(row.getString("username")));
+ }
+ logger.info("Completed conversion of legacy credentials");
+ }
+ }
+ catch (Exception e)
+ {
+ logger.info("Unable to complete conversion of legacy auth data (perhaps not enough nodes are upgraded yet). " +
+ "Conversion should not be considered complete");
+ logger.debug("Conversion error", e);
+ }
+ }
+
+ private CQLStatement prepare(String template, String keyspace, String table)
+ {
+ try
+ {
+ return QueryProcessor.parseStatement(String.format(template, keyspace, table)).prepare().statement;
+ }
+ catch (RequestValidationException e)
+ {
+ throw new AssertionError(e); // not supposed to happen
+ }
+ }
+
+ /*
+ * Retrieve all roles granted to the given role. includeInherited specifies
+ * whether to include only those roles granted directly or all inherited roles.
+ */
+ private void collectRoles(Role role, Set<RoleResource> collected, boolean includeInherited) throws RequestValidationException, RequestExecutionException
+ {
+ for (String memberOf : role.memberOf)
+ {
+ Role granted = getRole(memberOf);
+ if (role.equals(NULL_ROLE))
+ continue;
+ collected.add(RoleResource.role(granted.name));
+ if (includeInherited)
+ collectRoles(granted, collected, true);
+ }
+ }
+
+ /*
+ * Get a single Role instance given the role name. This never returns null, instead it
+ * uses the null object NULL_ROLE when a role with the given name cannot be found. So
+ * it's always safe to call methods on the returned object without risk of NPE.
+ */
+ private Role getRole(String name)
+ {
+ try
+ {
+ // If it exists, try the legacy users table in case the cluster
+ // is in the process of being upgraded and so is running with mixed
+ // versions of the authn schema.
+ return (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") != null)
+ ? getRoleFromTable(name, legacySelectUserStatement, LEGACY_ROW_TO_ROLE)
+ : getRoleFromTable(name, loadRoleStatement, ROW_TO_ROLE);
+ }
+ catch (RequestExecutionException | RequestValidationException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Role getRoleFromTable(String name, SelectStatement statement, Function<UntypedResultSet.Row, Role> function)
+ throws RequestExecutionException, RequestValidationException
+ {
+ ResultMessage.Rows rows =
+ statement.execute(QueryState.forInternalCalls(),
+ QueryOptions.forInternalCalls(consistencyForRole(name),
+ Collections.singletonList(ByteBufferUtil.bytes(name))));
+ if (rows.result.isEmpty())
+ return NULL_ROLE;
+
+ return function.apply(UntypedResultSet.create(rows.result).one());
+ }
+
+ /*
+ * Adds or removes a role name from the membership list of an entry in the roles table table
+ * (adds if op is "+", removes if op is "-")
+ */
+ private void modifyRoleMembership(String grantee, String role, String op)
+ throws RequestExecutionException
+ {
+ process(String.format("UPDATE %s.%s SET member_of = member_of %s {'%s'} WHERE role = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLES,
+ op,
+ escape(role),
+ escape(grantee)),
+ consistencyForRole(grantee));
+ }
+
+ /*
+ * Clear the membership list of the given role
+ */
+ private void removeAllMembers(String role) throws RequestValidationException, RequestExecutionException
+ {
+ // Get the membership list of the the given role
+ UntypedResultSet rows = process(String.format("SELECT member FROM %s.%s WHERE role = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLE_MEMBERS,
+ escape(role)),
+ consistencyForRole(role));
+ if (rows.isEmpty())
+ return;
+
+ // Update each member in the list, removing this role from its own list of granted roles
+ for (UntypedResultSet.Row row : rows)
+ modifyRoleMembership(row.getString("member"), role, "-");
+
+ // Finally, remove the membership list for the dropped role
+ process(String.format("DELETE FROM %s.%s WHERE role = '%s'",
+ AuthKeyspace.NAME,
+ AuthKeyspace.ROLE_MEMBERS,
+ escape(role)),
+ consistencyForRole(role));
+ }
+
+ /*
+ * Convert a map of Options from a CREATE/ALTER statement into
+ * assignment clauses used to construct a CQL UPDATE statement
+ */
+ private Iterable<String> optionsToAssignments(Map<Option, Object> options)
+ {
+ return Iterables.transform(
+ options.entrySet(),
+ new Function<Map.Entry<Option, Object>, String>()
+ {
+ public String apply(Map.Entry<Option, Object> entry)
+ {
+ switch (entry.getKey())
+ {
+ case LOGIN:
+ return String.format("can_login = %s", entry.getValue());
+ case SUPERUSER:
+ return String.format("is_superuser = %s", entry.getValue());
+ case PASSWORD:
+ return String.format("salted_hash = '%s'", escape(hashpw((String) entry.getValue())));
+ default:
+ return null;
+ }
+ }
+ });
+ }
+
+ protected static ConsistencyLevel consistencyForRole(String role)
+ {
+ if (role.equals(DEFAULT_SUPERUSER_NAME))
+ return ConsistencyLevel.QUORUM;
+ else
+ return ConsistencyLevel.LOCAL_ONE;
+ }
+
+ private static String hashpw(String password)
+ {
+ return BCrypt.hashpw(password, BCrypt.gensalt(GENSALT_LOG2_ROUNDS));
+ }
+
+ private static String escape(String name)
+ {
+ return StringUtils.replace(name, "'", "''");
+ }
+
+ private static UntypedResultSet process(String query, ConsistencyLevel consistencyLevel) throws RequestExecutionException
+ {
+ return QueryProcessor.process(query, consistencyLevel);
+ }
+
+ private static final class Role
+ {
+ private String name;
+ private final boolean isSuper;
+ private final boolean canLogin;
+ private Set<String> memberOf;
+
+ private Role(String name, boolean isSuper, boolean canLogin, Set<String> memberOf)
+ {
+ this.name = name;
+ this.isSuper = isSuper;
+ this.canLogin = canLogin;
+ this.memberOf = memberOf;
+ }
+
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ return true;
+
+ if (!(o instanceof Role))
+ return false;
+
+ Role r = (Role) o;
+ return Objects.equal(name, r.name);
+ }
+
+ public int hashCode()
+ {
+ return Objects.hashCode(name);
+ }
+ }
+}
[2/3] cassandra git commit: Make PasswordAuthenticator number of
hashing rounds configurable
Posted by sn...@apache.org.
Make PasswordAuthenticator number of hashing rounds configurable
Patch by Sam Tunnicliffe; Reviewed by Robert Stupp for CASSANDRA-8085
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/bf1ea024
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/bf1ea024
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/bf1ea024
Branch: refs/heads/trunk
Commit: bf1ea024d50dc0243bb8cec2a23b01a709b4e705
Parents: bcf0ec6
Author: Sam Tunnicliffe <sa...@beobal.com>
Authored: Tue Mar 24 14:14:18 2015 +0100
Committer: Robert Stupp <sn...@snazy.de>
Committed: Tue Mar 24 14:14:18 2015 +0100
----------------------------------------------------------------------
CHANGES.txt | 1 +
.../cassandra/auth/PasswordAuthenticator.java | 16 ++++++++++++++--
2 files changed, 15 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cassandra/blob/bf1ea024/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 0e75973..e85b622 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
2.1.4
+ * Make PasswordAuthenticator number of hashing rounds configurable (CASSANDRA-8085)
* Fix AssertionError when binding nested collections in DELETE (CASSANDRA-8900)
* Check for overlap with non-early sstables in LCS (CASSANDRA-8739)
* Only calculate max purgable timestamp if we have to (CASSANDRA-8914)
http://git-wip-us.apache.org/repos/asf/cassandra/blob/bf1ea024/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
index 9570770..7d30664 100644
--- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@ -31,6 +31,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.config.Config;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.QueryOptions;
@@ -51,8 +52,19 @@ public class PasswordAuthenticator implements ISaslAwareAuthenticator
{
private static final Logger logger = LoggerFactory.getLogger(PasswordAuthenticator.class);
- // 2 ** GENSALT_LOG2_ROUNS rounds of hashing will be performed.
- private static final int GENSALT_LOG2_ROUNDS = 10;
+ // 2 ** GENSALT_LOG2_ROUNDS rounds of hashing will be performed.
+ private static final String GENSALT_LOG2_ROUNDS_PROPERTY = Config.PROPERTY_PREFIX + "auth_bcrypt_gensalt_log2_rounds";
+ private static final int GENSALT_LOG2_ROUNDS = getGensaltLogRounds();
+
+ static int getGensaltLogRounds()
+ {
+ int rounds = Integer.getInteger(GENSALT_LOG2_ROUNDS_PROPERTY, 10);
+ if (rounds < 4 || rounds > 31)
+ throw new RuntimeException(new ConfigurationException(String.format("Bad value for system property -D%s. " +
+ "Please use a value 4 and 31",
+ GENSALT_LOG2_ROUNDS_PROPERTY)));
+ return rounds;
+ }
// name of the hash column.
private static final String SALTED_HASH = "salted_hash";