mirror of https://github.com/apache/cassandra
Add LIST SUPERUSERS CQL statement
patch by Shailaja Koppu; reviewed by Stefan Miklosovic and Benjamin Lerer for CASSANDRA-19417
This commit is contained in:
parent
da86cddddb
commit
b35ad427c5
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
list_superusers_statement ::= LIST SUPERUSERS
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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'''
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue