Allow to grant permission for all tables in a keyspace

Patch by Benjamin Lerer; Review by Andres de la Peña and Ekaterina Dimitrova for CASSANDRA-17027

In some cases it is useful to prevent users to alter or drop a keyspace
while allowing them to create new tables.
This patch add support for a new DataResource below KEYSPACE but above
TABLE. The syntax to grant permission at this level in ALL TABLES IN
KEYSPACE.
This commit is contained in:
Benjamin Lerer 2021-10-08 17:32:15 +02:00
parent 3a950b45c3
commit df6ecaae1c
16 changed files with 569 additions and 28 deletions

View File

@ -1,4 +1,5 @@
4.1
* Allow to grant permission for all tables in a keyspace (CASSANDRA-17027)
* Log time spent writing keys during compaction (CASSANDRA-17037)
* Make nodetool compactionstats and sstable_tasks consistent (CASSANDRA-16976)
* Add metrics and logging around index summary redistribution (CASSANDRA-17036)

View File

@ -38,6 +38,8 @@ using the provided 'sstableupgrade' tool.
New features
------------
- A new ALL TABLES IN KEYSPACE resource has been added. It allows to grant permissions for all tables and user types
in a keyspace while preventing the user to use those permissions on the keyspace itself.
- Added support for type casting in the WHERE clause components and in the values of INSERT and UPDATE statements.
- Warn/abort thresholds added to read queries notifying clients when these thresholds trigger (by
emitting a client warning or aborting the query). This feature is disabled by default, scheduled

View File

@ -1524,6 +1524,7 @@ syntax_rules += r'''
<dataResource> ::= ( "ALL" "KEYSPACES" )
| ( "KEYSPACE" <keyspaceName> )
| ( "ALL" "TABLES" "IN" "KEYSPACE" <keyspaceName> )
| ( "TABLE"? <columnFamilyName> )
;

View File

@ -826,3 +826,49 @@ class TestCqlshCompletion(CqlshCompletionCase):
self.trycompletions('ALTER KEYSPACE system_trac', "es WITH replication = {'class': '")
self.trycompletions("ALTER KEYSPACE system_traces WITH replication = {'class': '", '',
choices=['NetworkTopologyStrategy', 'SimpleStrategy'])
def test_complete_in_grant(self):
self.trycompletions("GR",
immediate='ANT ')
self.trycompletions("GRANT ",
choices=['ALL', 'ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'MODIFY', 'SELECT'],
other_choices_ok=True)
self.trycompletions("GRANT MODIFY ",
choices=['ON', 'PERMISSION'])
self.trycompletions("GRANT MODIFY P",
immediate='ERMISSION ON ')
self.trycompletions("GRANT MODIFY PERMISSION O",
immediate='N ')
self.trycompletions("GRANT MODIFY ON ",
choices=['ALL', 'KEYSPACE', 'MBEANS', 'ROLE', 'FUNCTION', 'MBEAN', 'TABLE'],
other_choices_ok=True)
self.trycompletions("GRANT MODIFY ON ALL ",
choices=['KEYSPACES', 'TABLES'],
other_choices_ok=True)
self.trycompletions("GRANT MODIFY PERMISSION ON KEY",
immediate='SPACE ')
self.trycompletions("GRANT MODIFY PERMISSION ON KEYSPACE system_tr",
immediate='aces TO ')
def test_complete_in_revoke(self):
self.trycompletions("RE",
immediate='VOKE ')
self.trycompletions("REVOKE ",
choices=['ALL', 'ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'MODIFY', 'SELECT'],
other_choices_ok=True)
self.trycompletions("REVOKE MODIFY ",
choices=['ON', 'PERMISSION'])
self.trycompletions("REVOKE MODIFY P",
immediate='ERMISSION ON ')
self.trycompletions("REVOKE MODIFY PERMISSION O",
immediate='N ')
self.trycompletions("REVOKE MODIFY PERMISSION ON ",
choices=['ALL', 'KEYSPACE', 'MBEANS', 'ROLE', 'FUNCTION', 'MBEAN', 'TABLE'],
other_choices_ok=True)
self.trycompletions("REVOKE MODIFY ON ALL ",
choices=['KEYSPACES', 'TABLES'],
other_choices_ok=True)
self.trycompletions("REVOKE MODIFY PERMISSION ON KEY",
immediate='SPACE ')
self.trycompletions("REVOKE MODIFY PERMISSION ON KEYSPACE system_tr",
immediate='aces FROM ')

View File

@ -1118,8 +1118,8 @@ resource returns [IResource res]
dataResource returns [DataResource res]
: K_ALL K_KEYSPACES { $res = DataResource.root(); }
| K_KEYSPACE ks = keyspaceName { $res = DataResource.keyspace($ks.id); }
| ( K_COLUMNFAMILY )? cf = columnFamilyName
{ $res = DataResource.table($cf.name.getKeyspace(), $cf.name.getName()); }
| ( K_COLUMNFAMILY )? cf = columnFamilyName { $res = DataResource.table($cf.name.getKeyspace(), $cf.name.getName()); }
| K_ALL K_TABLES K_IN K_KEYSPACE ks = keyspaceName { $res = DataResource.allTables($ks.id); }
;
jmxResource returns [JMXResource res]

View File

@ -31,6 +31,7 @@ public class AuthSchemaChangeListener extends SchemaChangeListener
public void onDropKeyspace(String ksName)
{
DatabaseDescriptor.getAuthorizer().revokeAllOn(DataResource.keyspace(ksName));
DatabaseDescriptor.getAuthorizer().revokeAllOn(DataResource.allTables(ksName));
DatabaseDescriptor.getAuthorizer().revokeAllOn(FunctionResource.keyspace(ksName));
}

View File

@ -137,25 +137,32 @@ public class CassandraRoleManager implements IRoleManager
public CassandraRoleManager()
{
supportedOptions = DatabaseDescriptor.getAuthenticator().getClass() == PasswordAuthenticator.class
supportedOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
? ImmutableSet.of(Option.LOGIN, Option.SUPERUSER, Option.PASSWORD)
: ImmutableSet.of(Option.LOGIN, Option.SUPERUSER);
alterableOptions = DatabaseDescriptor.getAuthenticator().getClass().equals(PasswordAuthenticator.class)
alterableOptions = DatabaseDescriptor.getAuthenticator() instanceof PasswordAuthenticator
? ImmutableSet.of(Option.PASSWORD)
: ImmutableSet.<Option>of();
}
@Override
public void setup()
{
loadRoleStatement = (SelectStatement) prepare("SELECT * from %s.%s WHERE role = ?",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES);
loadRoleStatement();
scheduleSetupTask(() -> {
setupDefaultRole();
return null;
});
}
protected final void loadRoleStatement()
{
loadRoleStatement = (SelectStatement) prepare("SELECT * from %s.%s WHERE role = ?",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES);
}
public Set<Option> supportedOptions()
{
return supportedOptions;
@ -344,12 +351,7 @@ public class CassandraRoleManager implements IRoleManager
{
if (!hasExistingRoles())
{
QueryProcessor.process(String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) " +
"VALUES ('%s', true, true, '%s')",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES,
DEFAULT_SUPERUSER_NAME,
escape(hashpw(DEFAULT_SUPERUSER_PASSWORD))),
QueryProcessor.process(createDefaultRoleQuery(),
consistencyForRoleWrite(DEFAULT_SUPERUSER_NAME));
logger.info("Created default superuser role '{}'", DEFAULT_SUPERUSER_NAME);
}
@ -361,6 +363,15 @@ public class CassandraRoleManager implements IRoleManager
}
}
protected static String createDefaultRoleQuery()
{
return String.format("INSERT INTO %s.%s (role, is_superuser, can_login, salted_hash) VALUES ('%s', true, true, '%s')",
SchemaConstants.AUTH_KEYSPACE_NAME,
AuthKeyspace.ROLES,
DEFAULT_SUPERUSER_NAME,
escape(hashpw(DEFAULT_SUPERUSER_PASSWORD)));
}
private static boolean hasExistingRoles() throws RequestExecutionException
{
// Try looking up the 'cassandra' default role first, to avoid the range query if possible.

View File

@ -31,13 +31,14 @@ import org.apache.cassandra.schema.Schema;
* Used to represent a table or a keyspace or the root level "data" resource.
* "data" - the root level data resource.
* "data/keyspace_name" - keyspace-level data resource.
* "data/keyspace_name/*" - all tables-level data resource.
* "data/keyspace_name/table_name" - table-level data resource.
*/
public class DataResource implements IResource
{
enum Level
{
ROOT, KEYSPACE, TABLE
ROOT, KEYSPACE, ALL_TABLES, TABLE
}
// permissions which may be granted on tables
@ -46,6 +47,15 @@ public class DataResource implements IResource
Permission.SELECT,
Permission.MODIFY,
Permission.AUTHORIZE);
// permissions which may be granted on all tables of a given keyspace
private static final Set<Permission> ALL_TABLES_LEVEL_PERMISSIONS = Sets.immutableEnumSet(Permission.CREATE,
Permission.ALTER,
Permission.DROP,
Permission.SELECT,
Permission.MODIFY,
Permission.AUTHORIZE);
// permissions which may be granted on one or all keyspaces
private static final Set<Permission> KEYSPACE_LEVEL_PERMISSIONS = Sets.immutableEnumSet(Permission.CREATE,
Permission.ALTER,
@ -91,6 +101,17 @@ public class DataResource implements IResource
return new DataResource(Level.KEYSPACE, keyspace, null);
}
/**
* Creates a DataResource representing all tables of a keyspace.
*
* @param keyspace Name of the keyspace.
* @return DataResource instance representing the keyspace.
*/
public static DataResource allTables(String keyspace)
{
return new DataResource(Level.ALL_TABLES, keyspace, null);
}
/**
* Creates a DataResource instance representing a table.
*
@ -122,6 +143,9 @@ public class DataResource implements IResource
if (parts.length == 2)
return keyspace(parts[1]);
if ("*".equals(parts[2]))
return allTables(parts[1]);
return table(parts[1], parts[2]);
}
@ -136,6 +160,8 @@ public class DataResource implements IResource
return ROOT_NAME;
case KEYSPACE:
return String.format("%s/%s", ROOT_NAME, keyspace);
case ALL_TABLES:
return String.format("%s/%s/*", ROOT_NAME, keyspace);
case TABLE:
return String.format("%s/%s/%s", ROOT_NAME, keyspace, table);
}
@ -151,8 +177,10 @@ public class DataResource implements IResource
{
case KEYSPACE:
return root();
case TABLE:
case ALL_TABLES:
return keyspace(keyspace);
case TABLE:
return allTables(keyspace);
}
throw new IllegalStateException("Root-level resource can't have a parent");
}
@ -167,6 +195,11 @@ public class DataResource implements IResource
return level == Level.KEYSPACE;
}
public boolean isAllTablesLevel()
{
return level == Level.ALL_TABLES;
}
public boolean isTableLevel()
{
return level == Level.TABLE;
@ -209,6 +242,7 @@ public class DataResource implements IResource
case ROOT:
return true;
case KEYSPACE:
case ALL_TABLES:
return Schema.instance.getKeyspaces().contains(keyspace);
case TABLE:
return Schema.instance.getTableMetadata(keyspace, table) != null;
@ -223,6 +257,8 @@ public class DataResource implements IResource
case ROOT:
case KEYSPACE:
return KEYSPACE_LEVEL_PERMISSIONS;
case ALL_TABLES:
return ALL_TABLES_LEVEL_PERMISSIONS;
case TABLE:
return TABLE_LEVEL_PERMISSIONS;
}
@ -238,6 +274,8 @@ public class DataResource implements IResource
return "<all keyspaces>";
case KEYSPACE:
return String.format("<keyspace %s>", keyspace);
case ALL_TABLES:
return String.format("<all tables in %s>", keyspace);
case TABLE:
return String.format("<table %s.%s>", keyspace, table);
}

View File

@ -84,6 +84,7 @@ public class CreateRoleStatement extends AuthenticationStatement
DatabaseDescriptor.getNetworkAuthorizer().setRoleDatacenters(role, dcPermissions);
}
grantPermissionsToCreator(state);
return null;
}

View File

@ -58,7 +58,7 @@ public abstract class AlterTypeStatement extends AlterSchemaStatement
public void authorize(ClientState client)
{
client.ensureKeyspacePermission(keyspaceName, Permission.ALTER);
client.ensureAllTablesPermission(keyspaceName, Permission.ALTER);
}
SchemaChange schemaChangeEvent(Keyspaces.KeyspacesDiff diff)

View File

@ -125,7 +125,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
public void authorize(ClientState client)
{
client.ensureKeyspacePermission(keyspaceName, Permission.CREATE);
client.ensureAllTablesPermission(keyspaceName, Permission.CREATE);
}
@Override

View File

@ -106,7 +106,7 @@ public final class CreateTypeStatement extends AlterSchemaStatement
public void authorize(ClientState client)
{
client.ensureKeyspacePermission(keyspaceName, Permission.CREATE);
client.ensureAllTablesPermission(keyspaceName, Permission.CREATE);
}
@Override

View File

@ -120,7 +120,7 @@ public final class DropTypeStatement extends AlterSchemaStatement
public void authorize(ClientState client)
{
client.ensureKeyspacePermission(keyspaceName, Permission.DROP);
client.ensureAllTablesPermission(keyspaceName, Permission.DROP);
}
@Override

View File

@ -366,6 +366,11 @@ public class ClientState
ensurePermission(keyspace, perm, DataResource.keyspace(keyspace));
}
public void ensureAllTablesPermission(String keyspace, Permission perm)
{
ensurePermission(keyspace, perm, DataResource.allTables(keyspace));
}
public void ensureTablePermission(String keyspace, String table, Permission perm)
{
ensurePermission(keyspace, perm, DataResource.table(keyspace, table));

View File

@ -0,0 +1,302 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.auth;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLTester;
public class GrantAndRevokeTest extends CQLTester
{
private static final String user = "user";
private static final String pass = "12345";
@BeforeClass
public static void setUpClass()
{
DatabaseDescriptor.setPermissionsValidity(0);
CQLTester.setUpClass();
requireAuthentication();
requireNetwork();
}
@After
public void tearDown() throws Throwable
{
useSuperUser();
executeNet("DROP ROLE " + user);
}
@Test
public void testGrantedKeyspace() throws Throwable
{
useSuperUser();
executeNet(String.format("CREATE ROLE %s WITH LOGIN = TRUE AND password='%s'", user, pass));
executeNet("GRANT CREATE ON KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
String table = KEYSPACE_PER_TEST + '.' + createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck int, val int, val_2 text, PRIMARY KEY (pk, ck))");
String index = KEYSPACE_PER_TEST + '.' + createIndex(KEYSPACE_PER_TEST, "CREATE INDEX ON %s (val_2)");
String type = KEYSPACE_PER_TEST + '.' + createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (a int, b text)");
String mv = KEYSPACE_PER_TEST + ".ks_mv_01";
executeNet("CREATE MATERIALIZED VIEW " + mv + " AS SELECT * FROM " + table + " WHERE val IS NOT NULL AND pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (val, pk, ck)");
useUser(user, pass);
// ALTER and DROP tables created by somebody else
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "INSERT INTO %s (pk, ck, val, val_2) VALUES (1, 1, 1, '1')"));
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "UPDATE %s SET val = 1 WHERE pk = 1 AND ck = 1"));
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "DELETE FROM %s WHERE pk = 1 AND ck = 2"));
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "SELECT * FROM %s WHERE pk = 1 AND ck = 1"));
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
"SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "TRUNCATE TABLE %s"));
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE %s ADD val_3 int"));
assertUnauthorizedQuery("User user has no DROP permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "DROP TABLE %s"));
assertUnauthorizedQuery("User user has no ALTER permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"ALTER TYPE " + type + " ADD c bigint");
assertUnauthorizedQuery("User user has no DROP permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"DROP TYPE " + type);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP MATERIALIZED VIEW " + mv);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP INDEX " + index);
useSuperUser();
executeNet("GRANT ALTER ON KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
executeNet("GRANT DROP ON KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
executeNet("GRANT SELECT ON KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
executeNet("GRANT MODIFY ON KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
useUser(user, pass);
executeNet("ALTER KEYSPACE " + KEYSPACE_PER_TEST + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
executeNet(formatQuery(KEYSPACE_PER_TEST, "INSERT INTO %s (pk, ck, val, val_2) VALUES (1, 1, 1, '1')"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "UPDATE %s SET val = 1 WHERE pk = 1 AND ck = 1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "DELETE FROM %s WHERE pk = 1 AND ck = 2"));
assertRowsNet(executeNet(formatQuery(KEYSPACE_PER_TEST, "SELECT * FROM %s WHERE pk = 1 AND ck = 1")), row(1, 1, 1, "1"));
assertRowsNet(executeNet("SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1"), row(1, 1, 1, "1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "TRUNCATE TABLE %s"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE %s ADD val_3 int"));
executeNet("DROP MATERIALIZED VIEW " + mv);
executeNet("DROP INDEX " + index);
executeNet(formatQuery(KEYSPACE_PER_TEST, "DROP TABLE %s"));
executeNet("ALTER TYPE " + type + " ADD c bigint");
executeNet("DROP TYPE " + type);
// calling creatTableName to create a new table name that will be used by the formatQuery
table = createTableName();
type = KEYSPACE_PER_TEST + "." + createTypeName();
mv = KEYSPACE_PER_TEST + ".ks_mv_02";
executeNet("CREATE TYPE " + type + " (a int, b text)");
executeNet(formatQuery(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck int, val int, val_2 text, PRIMARY KEY (pk, ck))"));
executeNet("CREATE MATERIALIZED VIEW " + mv + " AS SELECT * FROM " + table + " WHERE val IS NOT NULL AND pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (val, pk, ck)");
executeNet(formatQuery(KEYSPACE_PER_TEST, "INSERT INTO %s (pk, ck, val, val_2) VALUES (1, 1, 1, '1')"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "UPDATE %s SET val = 1 WHERE pk = 1 AND ck = 1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "DELETE FROM %s WHERE pk = 1 AND ck = 2"));
assertRowsNet(executeNet(formatQuery(KEYSPACE_PER_TEST, "SELECT * FROM %s WHERE pk = 1 AND ck = 1")), row(1, 1, 1, "1"));
assertRowsNet(executeNet("SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1"), row(1, 1, 1, "1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "TRUNCATE TABLE %s"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE %s ADD val_3 int"));
executeNet("DROP MATERIALIZED VIEW " + mv);
executeNet(formatQuery(KEYSPACE_PER_TEST, "DROP TABLE %s"));
executeNet("ALTER TYPE " + type + " ADD c bigint");
executeNet("DROP TYPE " + type);
useSuperUser();
executeNet("REVOKE ALTER ON KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
executeNet("REVOKE DROP ON KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
executeNet("REVOKE SELECT ON KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
executeNet("REVOKE MODIFY ON KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
table = KEYSPACE_PER_TEST + "." + createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck int, val int, val_2 text, PRIMARY KEY (pk, ck))");
type = KEYSPACE_PER_TEST + "." + createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (a int, b text)");
index = KEYSPACE_PER_TEST + '.' + createIndex(KEYSPACE_PER_TEST, "CREATE INDEX ON %s (val_2)");
mv = KEYSPACE_PER_TEST + ".ks_mv_03";
executeNet("CREATE MATERIALIZED VIEW " + mv + " AS SELECT * FROM " + table + " WHERE val IS NOT NULL AND pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (val, pk, ck)");
useUser(user, pass);
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"INSERT INTO " + table + " (pk, ck, val, val_2) VALUES (1, 1, 1, '1')");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"UPDATE " + table + " SET val = 1 WHERE pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"DELETE FROM " + table + " WHERE pk = 1 AND ck = 2");
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
"SELECT * FROM " + table + " WHERE pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
"SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"TRUNCATE TABLE " + table);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE " + table + " ADD val_3 int"));
assertUnauthorizedQuery("User user has no DROP permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "DROP TABLE " + table));
assertUnauthorizedQuery("User user has no ALTER permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"ALTER TYPE " + type + " ADD c bigint");
assertUnauthorizedQuery("User user has no DROP permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"DROP TYPE " + type);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP MATERIALIZED VIEW " + mv);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP INDEX " + index);
}
@Test
public void testGrantedAllTables() throws Throwable
{
useSuperUser();
executeNet(String.format("CREATE ROLE %s WITH LOGIN = TRUE AND password='%s'", user, pass));
executeNet("GRANT CREATE ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
String table = KEYSPACE_PER_TEST + "." + createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck int, val int, val_2 text, PRIMARY KEY (pk, ck))");
String index = KEYSPACE_PER_TEST + '.' + createIndex(KEYSPACE_PER_TEST, "CREATE INDEX ON %s (val_2)");
String type = KEYSPACE_PER_TEST + "." + createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (a int, b text)");
String mv = KEYSPACE_PER_TEST + ".alltables_mv_01";
executeNet("CREATE MATERIALIZED VIEW " + mv + " AS SELECT * FROM " + table + " WHERE val IS NOT NULL AND pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (val, pk, ck)");
useUser(user, pass);
// ALTER and DROP tables created by somebody else
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "INSERT INTO %s (pk, ck, val, val_2) VALUES (1, 1, 1, '1')"));
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "UPDATE %s SET val = 1 WHERE pk = 1 AND ck = 1"));
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "DELETE FROM %s WHERE pk = 1 AND ck = 2"));
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "SELECT * FROM %s WHERE pk = 1 AND ck = 1"));
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
"SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "TRUNCATE TABLE %s"));
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE %s ADD val_3 int"));
assertUnauthorizedQuery("User user has no DROP permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "DROP TABLE %s"));
assertUnauthorizedQuery("User user has no ALTER permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"ALTER TYPE " + type + " ADD c bigint");
assertUnauthorizedQuery("User user has no DROP permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"DROP TYPE " + type);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP MATERIALIZED VIEW " + mv);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP INDEX " + index);
useSuperUser();
executeNet("GRANT ALTER ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
executeNet("GRANT DROP ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
executeNet("GRANT SELECT ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
executeNet("GRANT MODIFY ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " TO " + user);
useUser(user, pass);
assertUnauthorizedQuery("User user has no ALTER permission on <keyspace " + KEYSPACE_PER_TEST + "> or any of its parents",
"ALTER KEYSPACE " + KEYSPACE_PER_TEST + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}");
executeNet(formatQuery(KEYSPACE_PER_TEST, "INSERT INTO %s (pk, ck, val, val_2) VALUES (1, 1, 1, '1')"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "UPDATE %s SET val = 1 WHERE pk = 1 AND ck = 1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "DELETE FROM %s WHERE pk = 1 AND ck = 2"));
assertRowsNet(executeNet(formatQuery(KEYSPACE_PER_TEST, "SELECT * FROM %s WHERE pk = 1 AND ck = 1")), row(1, 1, 1, "1"));
assertRowsNet(executeNet("SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1"), row(1, 1, 1, "1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "TRUNCATE TABLE %s"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE %s ADD val_3 int"));
executeNet("DROP MATERIALIZED VIEW " + mv);
executeNet("DROP INDEX " + index);
executeNet(formatQuery(KEYSPACE_PER_TEST, "DROP TABLE %s"));
executeNet("ALTER TYPE " + type + " ADD c bigint");
executeNet("DROP TYPE " + type);
// calling creatTableName to create a new table name that will be used by the formatQuery
table = createTableName();
type = KEYSPACE_PER_TEST + "." + createTypeName();
mv = KEYSPACE_PER_TEST + ".alltables_mv_02";
executeNet("CREATE TYPE " + type + " (a int, b text)");
executeNet(formatQuery(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck int, val int, val_2 text, PRIMARY KEY (pk, ck))"));
index = KEYSPACE_PER_TEST + '.' + createIndex(KEYSPACE_PER_TEST, "CREATE INDEX ON %s (val_2)");
executeNet("CREATE MATERIALIZED VIEW " + mv + " AS SELECT * FROM " + table + " WHERE val IS NOT NULL AND pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (val, pk, ck)");
executeNet(formatQuery(KEYSPACE_PER_TEST, "INSERT INTO %s (pk, ck, val, val_2) VALUES (1, 1, 1, '1')"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "UPDATE %s SET val = 1 WHERE pk = 1 AND ck = 1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "DELETE FROM %s WHERE pk = 1 AND ck = 2"));
assertRowsNet(executeNet(formatQuery(KEYSPACE_PER_TEST, "SELECT * FROM %s WHERE pk = 1 AND ck = 1")), row(1, 1, 1, "1"));
assertRowsNet(executeNet("SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1"), row(1, 1, 1, "1"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "TRUNCATE TABLE %s"));
executeNet(formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE %s ADD val_3 int"));
executeNet("DROP MATERIALIZED VIEW " + mv);
executeNet("DROP INDEX " + index);
executeNet(formatQuery(KEYSPACE_PER_TEST, "DROP TABLE %s"));
executeNet("ALTER TYPE " + type + " ADD c bigint");
executeNet("DROP TYPE " + type);
useSuperUser();
executeNet("REVOKE ALTER ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
executeNet("REVOKE DROP ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
executeNet("REVOKE SELECT ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
executeNet("REVOKE MODIFY ON ALL TABLES IN KEYSPACE " + KEYSPACE_PER_TEST + " FROM " + user);
table = KEYSPACE_PER_TEST + "." + createTable(KEYSPACE_PER_TEST, "CREATE TABLE %s (pk int, ck int, val int, val_2 text, PRIMARY KEY (pk, ck))");
index = KEYSPACE_PER_TEST + '.' + createIndex(KEYSPACE_PER_TEST, "CREATE INDEX ON %s (val_2)");
type = KEYSPACE_PER_TEST + "." + createType(KEYSPACE_PER_TEST, "CREATE TYPE %s (a int, b text)");
mv = KEYSPACE_PER_TEST + ".alltables_mv_03";
executeNet("CREATE MATERIALIZED VIEW " + mv + " AS SELECT * FROM " + table + " WHERE val IS NOT NULL AND pk IS NOT NULL AND ck IS NOT NULL PRIMARY KEY (val, pk, ck)");
useUser(user, pass);
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"INSERT INTO " + table + " (pk, ck, val, val_2) VALUES (1, 1, 1, '1')");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"UPDATE " + table + " SET val = 1 WHERE pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"DELETE FROM " + table + " WHERE pk = 1 AND ck = 2");
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
"SELECT * FROM " + table + " WHERE pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no SELECT permission on <table " + table + "> or any of its parents",
"SELECT * FROM " + mv + " WHERE val = 1 AND pk = 1 AND ck = 1");
assertUnauthorizedQuery("User user has no MODIFY permission on <table " + table + "> or any of its parents",
"TRUNCATE TABLE " + table);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "ALTER TABLE " + table + " ADD val_3 int"));
assertUnauthorizedQuery("User user has no DROP permission on <table " + table + "> or any of its parents",
formatQuery(KEYSPACE_PER_TEST, "DROP TABLE " + table));
assertUnauthorizedQuery("User user has no ALTER permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"ALTER TYPE " + type + " ADD c bigint");
assertUnauthorizedQuery("User user has no DROP permission on <all tables in " + KEYSPACE_PER_TEST + "> or any of its parents",
"DROP TYPE " + type);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP MATERIALIZED VIEW " + mv);
assertUnauthorizedQuery("User user has no ALTER permission on <table " + table + "> or any of its parents",
"DROP INDEX " + index);
}
}

View File

@ -48,7 +48,9 @@ import com.google.common.collect.ImmutableSet;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.util.File;
import org.junit.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -56,9 +58,14 @@ import com.codahale.metrics.Gauge;
import com.datastax.driver.core.*;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.exceptions.UnauthorizedException;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.auth.AuthSchemaChangeListener;
import org.apache.cassandra.auth.AuthTestUtils;
import org.apache.cassandra.auth.IRoleManager;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
import org.apache.cassandra.db.virtual.VirtualSchemaKeyspace;
@ -92,6 +99,7 @@ import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.JMXServerUtils;
import org.assertj.core.api.Assertions;
import org.apache.cassandra.utils.Pair;
import static com.datastax.driver.core.SocketOptions.DEFAULT_CONNECT_TIMEOUT_MILLIS;
import static com.datastax.driver.core.SocketOptions.DEFAULT_READ_TIMEOUT_MILLIS;
@ -105,6 +113,11 @@ import static org.junit.Assert.fail;
*/
public abstract class CQLTester
{
/**
* The super user
*/
private static final User SUPER_USER = new User("cassandra", "cassandra");
protected static final Logger logger = LoggerFactory.getLogger(CQLTester.class);
public static final String KEYSPACE = "cql_test_keyspace";
@ -127,8 +140,8 @@ public abstract class CQLTester
protected static final int nativePort;
protected static final InetAddress nativeAddr;
protected static final Set<InetAddressAndPort> remoteAddrs = new HashSet<>();
private static final Map<ProtocolVersion, Cluster> clusters = new HashMap<>();
private static final Map<ProtocolVersion, Session> sessions = new HashMap<>();
private static final Map<Pair<User, ProtocolVersion>, Cluster> clusters = new HashMap<>();
private static final Map<Pair<User, ProtocolVersion>, Session> sessions = new HashMap<>();
private static Consumer<Cluster.Builder> clusterBuilderConfigurator;
@ -170,6 +183,8 @@ public abstract class CQLTester
private List<String> functions = new ArrayList<>();
private List<String> aggregates = new ArrayList<>();
private User user;
// We don't use USE_PREPARED_VALUES in the code below so some test can foce value preparation (if the result
// is not expected to be the same without preparation)
private boolean usePrepared = USE_PREPARED_VALUES;
@ -180,6 +195,24 @@ public abstract class CQLTester
return usePrepared;
}
/**
* Use the specified user for executing the queries over the network.
* @param username the user name
* @param password the user password
*/
public void useUser(String username, String password)
{
this.user = new User(username, password);
}
/**
* Use the super user for executing the queries over the network.
*/
public void useSuperUser()
{
this.user = SUPER_USER;
}
/**
* Returns a port number that is automatically allocated,
* typically from an ephemeral port range.
@ -332,6 +365,7 @@ public abstract class CQLTester
types = null;
functions = null;
aggregates = null;
user = null;
// We want to clean up after the test, but dropping a table is rather long so just do that asynchronously
ScheduledExecutors.optionalTasks.execute(new Runnable()
@ -416,6 +450,32 @@ public abstract class CQLTester
return allArgs;
}
protected static void requireAuthentication()
{
DatabaseDescriptor.setAuthenticator(new AuthTestUtils.LocalPasswordAuthenticator());
DatabaseDescriptor.setAuthorizer(new AuthTestUtils.LocalCassandraAuthorizer());
DatabaseDescriptor.setNetworkAuthorizer(new AuthTestUtils.LocalCassandraNetworkAuthorizer());
// The CassandraRoleManager constructor set the supported and alterable options based on
// DatabaseDescriptor authenticator type so it needs to be created only after the authenticator is set.
IRoleManager roleManager = new AuthTestUtils.LocalCassandraRoleManager()
{
public void setup()
{
loadRoleStatement();
QueryProcessor.executeInternal(createDefaultRoleQuery());
}
};
DatabaseDescriptor.setRoleManager(roleManager);
MigrationManager.announceNewKeyspace(AuthKeyspace.metadata(), true);
DatabaseDescriptor.getRoleManager().setup();
DatabaseDescriptor.getAuthenticator().setup();
DatabaseDescriptor.getAuthorizer().setup();
DatabaseDescriptor.getNetworkAuthorizer().setup();
Schema.instance.registerListener(new AuthSchemaChangeListener());
}
/**
* Initialize Native Transport for test that need it.
*/
@ -483,7 +543,7 @@ public abstract class CQLTester
server.start();
}
private static Cluster initClientCluster(ProtocolVersion version)
private static Cluster initClientCluster(User user, ProtocolVersion version)
{
SocketOptions socketOptions =
new SocketOptions().setConnectTimeoutMillis(Integer.getInteger("cassandra.test.driver.connection_timeout_ms",
@ -499,6 +559,8 @@ public abstract class CQLTester
.withClusterName("Test Cluster")
.withPort(nativePort)
.withSocketOptions(socketOptions);
if (user != null)
builder.withCredentials(user.username, user.password);
if (version.isBeta())
builder = builder.allowBetaProtocolVersion();
@ -671,14 +733,25 @@ public abstract class CQLTester
protected String createType(String query)
{
String typeName = String.format("type_%02d", seqNumber.getAndIncrement());
String fullQuery = String.format(query, KEYSPACE + "." + typeName);
types.add(typeName);
return createType(KEYSPACE, query);
}
protected String createType(String keyspace, String query)
{
String typeName = createTypeName();
String fullQuery = String.format(query, keyspace + "." + typeName);
logger.info(fullQuery);
schemaChange(fullQuery);
return typeName;
}
protected String createTypeName()
{
String typeName = String.format("type_%02d", seqNumber.getAndIncrement());
types.add(typeName);
return typeName;
}
protected String createFunctionName(String keyspace)
{
return String.format("%s.function_%02d", keyspace, seqNumber.getAndIncrement());
@ -815,7 +888,12 @@ public abstract class CQLTester
protected String createIndex(String query)
{
String formattedQuery = formatQuery(query);
return createIndex(KEYSPACE, query);
}
protected String createIndex(String keyspace, String query)
{
String formattedQuery = formatQuery(keyspace, query);
return createFormattedIndex(formattedQuery);
}
@ -1031,12 +1109,12 @@ public abstract class CQLTester
private Session getSession(ProtocolVersion protocolVersion)
{
Cluster cluster = getCluster(protocolVersion);
return sessions.computeIfAbsent(protocolVersion, userProto -> cluster.connect());
return sessions.computeIfAbsent(Pair.create(user, protocolVersion), userProto -> cluster.connect());
}
private Cluster getCluster(ProtocolVersion protocolVersion)
{
return clusters.computeIfAbsent(protocolVersion, userProto -> initClientCluster(protocolVersion));
return clusters.computeIfAbsent(Pair.create(user, protocolVersion), userProto -> initClientCluster(userProto.left, userProto.right));
}
protected SimpleClient newSimpleClient(ProtocolVersion version) throws IOException
@ -1550,6 +1628,21 @@ public abstract class CQLTester
e.getMessage().contains(text));
}
/**
* Checks that the specified query is not authorized for the current user.
* @param errorMessage The expected error message
* @param query the query
* @param values the query parameters
*/
protected void assertUnauthorizedQuery(String errorMessage, String query, Object... values) throws Throwable
{
assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
errorMessage,
UnauthorizedException.class,
query,
values);
}
@FunctionalInterface
public interface CheckedFunction {
void apply() throws Throwable;
@ -2018,4 +2111,44 @@ public abstract class CQLTester
return "UserTypeValue" + toCQLString();
}
}
private static class User
{
/**
* The user name
*/
public final String username;
/**
* The user password
*/
public final String password;
public User(String username, String password)
{
this.username = username;
this.password = password;
}
@Override
public int hashCode()
{
return Objects.hashCode(username, password);
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof User))
return false;
User u = (User) o;
return Objects.equal(username, u.username)
&& Objects.equal(password, u.password);
}
}
}