This is an automated email from the ASF dual-hosted git repository. smiklosovic 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 b35ad427c5 Add LIST SUPERUSERS CQL statement b35ad427c5 is described below commit b35ad427c5e9282730682553b6dcf5d70b603e22 Author: Shailaja Koppu <s_ko...@apple.com> AuthorDate: Tue Mar 12 12:27:58 2024 +0000 Add LIST SUPERUSERS CQL statement patch by Shailaja Koppu; reviewed by Stefan Miklosovic and Benjamin Lerer for CASSANDRA-19417 --- CHANGES.txt | 1 + doc/cql3/CQL.textile | 16 ++- .../examples/BNF/list_superusers_statement.bnf | 1 + .../cassandra/pages/developing/cql/security.adoc | 12 ++ .../pages/reference/cql-commands/commands-toc.adoc | 3 + pylib/cqlshlib/cql3handling.py | 4 + pylib/cqlshlib/test/test_cqlsh_completion.py | 2 +- src/antlr/Lexer.g | 1 + src/antlr/Parser.g | 11 ++ .../apache/cassandra/audit/AuditLogEntryType.java | 3 +- src/java/org/apache/cassandra/auth/Roles.java | 14 +++ .../cql3/statements/ListSuperUsersStatement.java | 102 ++++++++++++++++ .../cassandra/audit/AuditLoggerAuthTest.java | 10 ++ test/unit/org/apache/cassandra/auth/RolesTest.java | 22 ++++ .../statements/ListSuperUsersStatementTest.java | 131 +++++++++++++++++++++ 15 files changed, 330 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 296d4cb2d9..b6b9ab86ba 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Add LIST SUPERUSERS CQL statement (CASSANDRA-19417) * Modernize CQLSH datetime conversions (CASSANDRA-18879) * Harry model and in-JVM tests for partition-restricted 2i queries (CASSANDRA-18275) * Refactor cqlshmain global constants (CASSANDRA-19201) diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 959533f771..c85fef5ab5 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -1414,7 +1414,7 @@ REVOKE report_writer FROM alice; This statement revokes the @report_writer@ role from @alice@. Any permissions that @alice@ has acquired via the @report_writer@ role are also revoked. -h4(#listRolesStmt). LIST ROLES +h3(#listRolesStmt). LIST ROLES __Syntax:__ @@ -1438,6 +1438,20 @@ LIST ROLES OF @bob@ NORECURSIVE List all roles directly granted to @bob@. +h3(#listSuperusersStmt). LIST SUPERUSERS + +__Syntax:__ + +bc(syntax). +<list-superusers-stmt> ::= LIST SUPERUSERS; + +__Sample:__ + +bc(sample). +LIST SUPERUSERS; + +Returns roles with the superuser privilege (this includes roles with transitively acquired superuser privilege), this command requires `DESCRIBE` permission on all roles of the database. + h3(#createUserStmt). CREATE USER Prior to the introduction of roles in Cassandra 2.2, authentication and authorization were based around the concept of a @USER@. For backward compatibility, the legacy syntax has been preserved with @USER@ centric statments becoming synonyms for the @ROLE@ based equivalents. diff --git a/doc/modules/cassandra/examples/BNF/list_superusers_statement.bnf b/doc/modules/cassandra/examples/BNF/list_superusers_statement.bnf new file mode 100644 index 0000000000..ae21466e19 --- /dev/null +++ b/doc/modules/cassandra/examples/BNF/list_superusers_statement.bnf @@ -0,0 +1 @@ +list_superusers_statement ::= LIST SUPERUSERS diff --git a/doc/modules/cassandra/pages/developing/cql/security.adoc b/doc/modules/cassandra/pages/developing/cql/security.adoc index f751a1658d..2d438b9815 100644 --- a/doc/modules/cassandra/pages/developing/cql/security.adoc +++ b/doc/modules/cassandra/pages/developing/cql/security.adoc @@ -261,6 +261,18 @@ transitively acquired ones: include::cassandra:example$CQL/list_roles_nonrecursive.cql[] ---- +[[list-superusers-statement]] +== LIST SUPERUSERS + +All the known roles (including transitively acquired) with superuser privilege can be listed using the `LIST SUPERUSERS` statement: + +[source, bnf] +---- +include::cassandra:example$BNF/list_superusers_statement.bnf[] +---- + +This command requires `DESCRIBE` permission on all roles of the database. + == Users Prior to the introduction of roles in Cassandra 2.2, authentication and diff --git a/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc b/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc index e1fe0fe0fd..0924e63ac7 100644 --- a/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc +++ b/doc/modules/cassandra/pages/reference/cql-commands/commands-toc.adoc @@ -95,6 +95,9 @@ Lists permissions on resources. xref:reference:cql-commands/list-roles.adoc[LIST ROLES] :: Lists roles and shows superuser and login status. +xref:reference:cql-commands/list-superusers.adoc[LIST SUPERUSERS] :: +Lists roles with the superuser privilege. + xref:reference:cql-commands/list-users.adoc[LIST USERS (Deprecated)] :: Lists existing internal authentication users and their superuser status. diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 252ebabe30..f795ee7dd0 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -302,6 +302,7 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ; | <alterRoleStatement> | <dropRoleStatement> | <listRolesStatement> + | <listSuperUsersStatement> ; <authorizationStatement> ::= <grantStatement> @@ -1529,6 +1530,9 @@ syntax_rules += r''' <listRolesStatement> ::= "LIST" "ROLES" ( "OF" <rolename> )? "NORECURSIVE"? ; + +<listSuperUsersStatement> ::= "LIST" "SUPERUSERS" + ; ''' syntax_rules += r''' diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py index c2a99dc7b5..793dcd7a8b 100644 --- a/pylib/cqlshlib/test/test_cqlsh_completion.py +++ b/pylib/cqlshlib/test/test_cqlsh_completion.py @@ -1053,7 +1053,7 @@ class TestCqlshCompletion(CqlshCompletionCase): def test_complete_in_list(self): - self.trycompletions('LIST ', choices=['ALL', 'AUTHORIZE', 'DESCRIBE', 'EXECUTE', 'ROLES', 'USERS', 'ALTER', 'CREATE', 'DROP', 'MODIFY', 'SELECT', 'UNMASK', 'SELECT_MASKED']) + self.trycompletions('LIST ', choices=['ALL', 'AUTHORIZE', 'DESCRIBE', 'EXECUTE', 'ROLES', 'USERS', 'ALTER', 'CREATE', 'DROP', 'MODIFY', 'SELECT', 'UNMASK', 'SELECT_MASKED', 'SUPERUSERS']) # Non-CQL Shell Commands diff --git a/src/antlr/Lexer.g b/src/antlr/Lexer.g index 17045b2578..f259307311 100644 --- a/src/antlr/Lexer.g +++ b/src/antlr/Lexer.g @@ -147,6 +147,7 @@ K_USER: U S E R; K_USERS: U S E R S; K_ROLE: R O L E; K_ROLES: R O L E S; +K_SUPERUSERS: S U P E R U S E R 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; diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index e33e61faa9..cb5b90c062 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -235,6 +235,7 @@ cqlStatement returns [CQLStatement.Raw stmt] | st41=describeStatement { $stmt = st41; } | st42=addIdentityStatement { $stmt = st42; } | st43=dropIdentityStatement { $stmt = st43; } + | st44=listSuperUsersStatement { $stmt = st44; } ; /* @@ -1357,6 +1358,15 @@ listRolesStatement returns [ListRolesStatement stmt] { $stmt = new ListRolesStatement(grantee, recursive); } ; +/** + * LIST SUPERUSERS + */ +listSuperUsersStatement returns [ListSuperUsersStatement stmt] + @init { + } + : K_LIST K_SUPERUSERS { $stmt = new ListSuperUsersStatement(); } + ; + roleOptions[RoleOptions opts, DCPermissions.Builder dcperms, CIDRPermissions.Builder cidrperms] : roleOption[opts, dcperms, cidrperms] (K_AND roleOption[opts, dcperms, cidrperms])* ; @@ -1961,6 +1971,7 @@ basic_unreserved_keyword returns [String str] | K_ROLES | K_IDENTITY | K_SUPERUSER + | K_SUPERUSERS | K_NOSUPERUSER | K_LOGIN | K_NOLOGIN diff --git a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java index 1055f875e0..17d4c98fea 100644 --- a/src/java/org/apache/cassandra/audit/AuditLogEntryType.java +++ b/src/java/org/apache/cassandra/audit/AuditLogEntryType.java @@ -70,7 +70,8 @@ public enum AuditLogEntryType REQUEST_FAILURE(AuditLogEntryCategory.ERROR), LOGIN_ERROR(AuditLogEntryCategory.AUTH), UNAUTHORIZED_ATTEMPT(AuditLogEntryCategory.AUTH), - LOGIN_SUCCESS(AuditLogEntryCategory.AUTH); + LOGIN_SUCCESS(AuditLogEntryCategory.AUTH), + LIST_SUPERUSERS(AuditLogEntryCategory.DCL); private final AuditLogEntryCategory category; diff --git a/src/java/org/apache/cassandra/auth/Roles.java b/src/java/org/apache/cassandra/auth/Roles.java index f18851af84..c4070aaff3 100644 --- a/src/java/org/apache/cassandra/auth/Roles.java +++ b/src/java/org/apache/cassandra/auth/Roles.java @@ -20,6 +20,7 @@ package org.apache.cassandra.auth; import java.util.Collections; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -82,6 +83,19 @@ public class Roles return cache.getAllRoles(); } + /** + * Gets all roles which pass the predicate. + * + * @param predicate a predicate to filter roles with + * @return unmodifiable set of role resources passing the predicate + */ + public static Set<RoleResource> getAllRoles(Predicate<RoleResource> predicate) + { + return getAllRoles().stream() + .filter(predicate) + .collect(Collectors.toUnmodifiableSet()); + } + /** * Returns true if the supplied role or any other role granted to it * (directly or indirectly) has superuser status. diff --git a/src/java/org/apache/cassandra/cql3/statements/ListSuperUsersStatement.java b/src/java/org/apache/cassandra/cql3/statements/ListSuperUsersStatement.java new file mode 100644 index 0000000000..3c94a14195 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/ListSuperUsersStatement.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.cql3.statements; + +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import org.apache.cassandra.audit.AuditLogContext; +import org.apache.cassandra.audit.AuditLogEntryType; +import org.apache.cassandra.auth.AuthKeyspace; +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.auth.RoleResource; +import org.apache.cassandra.auth.Roles; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.ResultSet; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.RequestExecutionException; +import org.apache.cassandra.exceptions.RequestValidationException; +import org.apache.cassandra.exceptions.UnauthorizedException; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.messages.ResultMessage; + +/** + * LIST SUPERUSERS cql command returns list of roles with superuser privileges + * This includes superusers and all roles who have superuser role granted in the roles hierarchy + */ +public class ListSuperUsersStatement extends AuthorizationStatement +{ + private static final List<ColumnSpecification> metadata = + List.of(new ColumnSpecification(SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES, + new ColumnIdentifier("role", true), UTF8Type.instance)); + + public ListSuperUsersStatement() + { + // nothing to do + } + + public void validate(ClientState state) throws UnauthorizedException, InvalidRequestException + { + state.ensureNotAnonymous(); + } + + public void authorize(ClientState state) throws InvalidRequestException + { + // Allow listing superuser privileged users only if the caller has DESCRIBE permission on 'all roles' + if (!DatabaseDescriptor.getAuthorizer() + .authorize(state.getUser(), RoleResource.root()) + .contains(Permission.DESCRIBE)) + { + throw new UnauthorizedException("You are not authorized to view superuser details"); + } + } + + public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException + { + Set<RoleResource> superUsers = Roles.getAllRoles(Roles::hasSuperuserStatus); + if (superUsers == null || superUsers.isEmpty()) + return new ResultMessage.Void(); + + ResultSet result = new ResultSet(new ResultSet.ResultMetadata(metadata)); + + superUsers.stream() + .sorted(RoleResource::compareTo) + .forEach(role -> result.addColumnValue(UTF8Type.instance.decompose(role.getRoleName()))); + + return new ResultMessage.Rows(result); + } + + @Override + public String toString() + { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + @Override + public AuditLogContext getAuditLogContext() + { + return new AuditLogContext(AuditLogEntryType.LIST_SUPERUSERS); + } +} diff --git a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java index 50d20ea883..c4a9819f56 100644 --- a/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java +++ b/test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java @@ -212,6 +212,16 @@ public class AuditLoggerAuthTest assertLogEntry(logEntry, AuditLogEntryType.LIST_ROLES, cql, CASS_USER, ""); } + @Test + public void testCqlLISTSUPERUSERSAuditing() + { + String cql = "LIST SUPERUSERS"; + executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS); + assertTrue(getInMemAuditLogger().size() > 0); + AuditLogEntry logEntry = getInMemAuditLogger().poll(); + assertLogEntry(logEntry, AuditLogEntryType.LIST_SUPERUSERS, cql, CASS_USER, ""); + } + @Test public void testCqlLISTPERMISSIONSAuditing() { diff --git a/test/unit/org/apache/cassandra/auth/RolesTest.java b/test/unit/org/apache/cassandra/auth/RolesTest.java index 7cdbaf2856..0a64d1ffc7 100644 --- a/test/unit/org/apache/cassandra/auth/RolesTest.java +++ b/test/unit/org/apache/cassandra/auth/RolesTest.java @@ -18,7 +18,10 @@ package org.apache.cassandra.auth; +import java.util.Arrays; +import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; import com.google.common.collect.Iterables; import org.junit.Assert; @@ -32,6 +35,8 @@ import org.apache.cassandra.db.ConsistencyLevel; import static org.apache.cassandra.auth.AuthTestUtils.ALL_ROLES; import static org.apache.cassandra.auth.AuthTestUtils.ROLE_A; import static org.apache.cassandra.auth.AuthTestUtils.ROLE_B; +import static org.apache.cassandra.auth.AuthTestUtils.ROLE_B_1; +import static org.apache.cassandra.auth.AuthTestUtils.ROLE_B_2; import static org.apache.cassandra.auth.AuthTestUtils.ROLE_C; import static org.apache.cassandra.auth.AuthTestUtils.getRolesReadCount; import static org.apache.cassandra.auth.AuthTestUtils.grantRolesTo; @@ -56,6 +61,13 @@ public class RolesTest roleManager.createRole(AuthenticatedUser.ANONYMOUS_USER, role, new RoleOptions()); grantRolesTo(roleManager, ROLE_A, ROLE_B, ROLE_C); + RoleOptions roleOptions = new RoleOptions(); + roleOptions.setOption(IRoleManager.Option.SUPERUSER, true); + RoleResource testSuperUser = RoleResource.role("testSuperuser"); + roleManager.createRole(AuthenticatedUser.ANONYMOUS_USER, testSuperUser, roleOptions); + grantRolesTo(roleManager, ROLE_B_1, testSuperUser); + grantRolesTo(roleManager, ROLE_B_2, ROLE_B_1); + roleManager.setup(); AuthCacheService.initializeAndRegisterCaches(); } @@ -117,4 +129,14 @@ public class RolesTest ConsistencyLevel nonPrivWriteLevel = CassandraRoleManager.consistencyForRoleWrite("non-privilaged"); Assert.assertEquals(nonPrivWriteLevel, DatabaseDescriptor.getAuthWriteConsistencyLevel()); } + + @Test + public void testSuperUsers() + { + Assert.assertEquals(new HashSet<>(Arrays.asList("testSuperuser", "role_b_1", "role_b_2")), + Roles.getAllRoles(Roles::hasSuperuserStatus) + .stream() + .map(RoleResource::getRoleName) + .collect(Collectors.toSet())); + } } diff --git a/test/unit/org/apache/cassandra/cql3/statements/ListSuperUsersStatementTest.java b/test/unit/org/apache/cassandra/cql3/statements/ListSuperUsersStatementTest.java new file mode 100644 index 0000000000..4b4c48b2a4 --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/statements/ListSuperUsersStatementTest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.cql3.statements; + +import java.util.Collections; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.exceptions.UnauthorizedException; +import org.apache.cassandra.auth.Roles; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.transport.messages.ResultMessage; +import org.mockito.MockedStatic; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mockStatic; + +public class ListSuperUsersStatementTest extends CQLTester +{ + @BeforeClass + public static void defineSchema() throws ConfigurationException + { + DatabaseDescriptor.setPermissionsValidity(0); + DatabaseDescriptor.setRolesValidity(0); + DatabaseDescriptor.setCredentialsValidity(0); + + requireAuthentication(); + requireNetwork(); + } + + @Test + public void testAcquiredSuperUsers() throws InterruptedException + { + useSuperUser(); + assertRowsNet(executeNet("list superusers"), row("cassandra")); + + executeNet("create role role1 with login=true and password='role1'"); + executeNet("create role role11 with login=true and password='role11'"); + executeNet("create role role2 with login=true and password='role2'"); + assertRowsNet(executeNet("list superusers"), row("cassandra")); + + executeNet("grant cassandra to role1"); + executeNet("grant role1 to role11"); + Roles.cache.invalidate(); + assertRowsNet(executeNet("list superusers"), row("cassandra"), row("role1"), row("role11")); + + useUser("role1", "role1"); + assertRowsNet(executeNet("list superusers"), row("cassandra"), row("role1"), row("role11")); + + useUser("role11", "role11"); + assertRowsNet(executeNet("list superusers"), row("cassandra"), row("role1"), row("role11")); + } + + @Test + public void testNoRoles() + { + try (MockedStatic<Roles> roles = mockStatic(Roles.class)) + { + roles.when(Roles::getAllRoles).thenReturn(Collections.emptySet()); + ClientState state = ClientState.forInternalCalls("system_auth"); + ListSuperUsersStatement listSuperUsersStatement = new ListSuperUsersStatement(); + ResultMessage result = listSuperUsersStatement.execute(state); + assertEquals("EMPTY RESULT", result.toString()); + } + } + + @Test + public void testGetAllRolesReturnsNull() + { + try (MockedStatic<Roles> roles = mockStatic(Roles.class)) + { + roles.when(Roles::getAllRoles).thenReturn(null); + ClientState state = ClientState.forInternalCalls("system_auth"); + ListSuperUsersStatement listSuperUsersStatement = new ListSuperUsersStatement(); + ResultMessage result = listSuperUsersStatement.execute(state); + assertEquals("EMPTY RESULT", result.toString()); + } + } + + @Test + public void testNonSuperUserDescribePermission() + { + useSuperUser(); + executeNet("create role nonsuper with login=true and password='nonsuper'"); + Roles.cache.invalidate(); + + useUser("nonsuper", "nonsuper"); + assertThatThrownBy(() -> executeNet("list superusers")) + .isInstanceOf(UnauthorizedException.class) + .hasMessage("You are not authorized to view superuser details"); + + useSuperUser(); + executeNet("GRANT DESCRIBE ON ALL ROLES to nonsuper"); + Roles.cache.invalidate(); + + useUser("nonsuper", "nonsuper"); + ResultSet result = executeNet("list superusers"); + // verify list command returned non-empty results + assertTrue(result.iterator().hasNext()); + } + + @Test + public void testListSuperUserStatementToString() + { + ListSuperUsersStatement listSuperUsersStatement = new ListSuperUsersStatement(); + assertEquals("ListSuperUsersStatement[bindVariables=<null>]", listSuperUsersStatement.toString()); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org