Add LIST SUPERUSERS CQL statement

patch by Shailaja Koppu; reviewed by Stefan Miklosovic and Benjamin Lerer for CASSANDRA-19417
This commit is contained in:
Shailaja Koppu 2024-03-12 12:27:58 +00:00 committed by Stefan Miklosovic
parent da86cddddb
commit b35ad427c5
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
15 changed files with 330 additions and 3 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@
list_superusers_statement ::= LIST SUPERUSERS

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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