This commit is contained in:
Štefan Miklošovič 2026-07-29 13:35:16 +08:00 committed by GitHub
commit 2273805fbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1105 additions and 315 deletions

File diff suppressed because it is too large Load Diff

View File

@ -199,7 +199,6 @@ New features
- Added a new CQL function, maxwritetime. It shows the largest unix timestamp that the data was written, similar to
its sibling CQL function, writetime.
- New Guardrails added:
- Whether ALTER TABLE commands are allowed to mutate columns
- Whether SimpleStrategy is allowed on keyspace creation or alteration
- Maximum replication factor
- Whether DROP KEYSPACE commands are allowed.

View File

@ -2084,6 +2084,10 @@ drop_compact_storage_enabled: false
# Guardrail to allow/disallow DROP KEYSPACE statements
# drop_keyspace_enabled: true
#
# Guardrail to allow/disallow DDL/DCL statements
# ddl_enabled: true
# dcl_enabled: true
#
# Guardrail to warn or fail when using a page size greater than threshold.
# The two thresholds default to -1 to disable.
# page_size_warn_threshold: -1
@ -2185,9 +2189,6 @@ drop_compact_storage_enabled: false
# vector_dimensions_warn_threshold: -1
# vector_dimensions_fail_threshold: -1
#
# Guardrail to indicate whether or not users are allowed to use ALTER TABLE commands to make column changes to tables
# alter_table_enabled: true
#
# Guardrail to warn or fail when local data disk usage percentage exceeds threshold. Valid values are in [1, 100].
# This is only used for the disks storing data directories, so it won't count any separate disks used for storing
# the commitlog, hints nor saved caches. The disk usage is the ratio between the amount of space used by the data

View File

@ -2056,6 +2056,10 @@ drop_compact_storage_enabled: false
# Guardrail to allow/disallow DROP KEYSPACE statements
# drop_keyspace_enabled: true
#
# Guardrail to allow/disallow DDL/DCL statements
# ddl_enabled: true
# dcl_enabled: true
#
# Guardrail to warn or fail when using a page size greater than threshold.
# The two thresholds default to -1 to disable.
# page_size_warn_threshold: -1
@ -2157,9 +2161,6 @@ drop_compact_storage_enabled: false
# vector_dimensions_warn_threshold: -1
# vector_dimensions_fail_threshold: -1
#
# Guardrail to indicate whether or not users are allowed to use ALTER TABLE commands to make column changes to tables
# alter_table_enabled: true
#
# Guardrail to warn or fail when local data disk usage percentage exceeds threshold. Valid values are in [1, 100].
# This is only used for the disks storing data directories, so it won't count any separate disks used for storing
# the commitlog, hints nor saved caches. The disk usage is the ratio between the amount of space used by the data

View File

@ -884,8 +884,9 @@ public class Config
public volatile Set<ConsistencyLevel> write_consistency_levels_warned = Collections.emptySet();
public volatile Set<ConsistencyLevel> write_consistency_levels_disallowed = Collections.emptySet();
public volatile boolean user_timestamps_enabled = true;
public volatile boolean alter_table_enabled = true;
public volatile boolean group_by_enabled = true;
public volatile boolean ddl_enabled = true;
public volatile boolean dcl_enabled = true;
public volatile boolean drop_truncate_table_enabled = true;
public volatile boolean drop_keyspace_enabled = true;
public volatile boolean secondary_indexes_enabled = true;

View File

@ -368,6 +368,34 @@ public class GuardrailsOptions implements GuardrailsConfig
x -> config.drop_keyspace_enabled = x);
}
@Override
public boolean getDDLEnabled()
{
return config.ddl_enabled;
}
public void setDDLEnabled(boolean enabled)
{
updatePropertyWithLogging("ddl_enabled",
enabled,
() -> config.ddl_enabled,
x -> config.ddl_enabled = x);
}
@Override
public boolean getDCLEnabled()
{
return config.dcl_enabled;
}
public void setDCLEnabled(boolean enabled)
{
updatePropertyWithLogging("dcl_enabled",
enabled,
() -> config.dcl_enabled,
x -> config.dcl_enabled = x);
}
@Override
public boolean getSecondaryIndexesEnabled()
{
@ -410,20 +438,6 @@ public class GuardrailsOptions implements GuardrailsConfig
x -> config.compact_tables_enabled = x);
}
@Override
public boolean getAlterTableEnabled()
{
return config.alter_table_enabled;
}
public void setAlterTableEnabled(boolean enabled)
{
updatePropertyWithLogging("alter_table_enabled",
enabled,
() -> config.alter_table_enabled,
x -> config.alter_table_enabled = x);
}
@Override
public boolean getReadBeforeWriteListOperationsEnabled()
{

View File

@ -100,6 +100,16 @@ public interface CQLStatement
return false;
}
default boolean isDDLStatement()
{
return false;
}
default boolean isDCLStatement()
{
return false;
}
public static abstract class Raw
{
protected VariableSpecifications bindVariables;

View File

@ -39,6 +39,7 @@ import org.antlr.runtime.*;
import org.apache.cassandra.concurrent.ImmediateExecutor;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.metrics.ClientRequestMetrics;
@ -271,6 +272,11 @@ public class QueryProcessor implements QueryHandler
statement.authorize(clientState);
statement.validate(clientState);
if (statement.isDDLStatement())
Guardrails.ddlEnabled.ensureEnabled(clientState);
else if (statement.isDCLStatement())
Guardrails.dclEnabled.ensureEnabled(clientState);
ResultMessage result = options.getConsistency() == ConsistencyLevel.NODE_LOCAL
? processNodeLocalStatement(statement, queryState, options)
: statement.execute(queryState, options, requestTime);

View File

@ -71,5 +71,11 @@ public abstract class AuthenticationStatement extends CQLStatement.Raw implement
{
return query;
}
@Override
public boolean isDCLStatement()
{
return true;
}
}

View File

@ -69,4 +69,10 @@ public abstract class AuthorizationStatement extends CQLStatement.Raw implements
{
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
@Override
public boolean isDCLStatement()
{
return true;
}
}

View File

@ -50,6 +50,12 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac
this.keyspaceName = keyspaceName;
}
@Override
public boolean isDDLStatement()
{
return true;
}
public void validate(ClientState state)
{
// validation is performed while executing the statement, in apply()

View File

@ -277,7 +277,6 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
{
Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state);
TableMetadata.Builder tableBuilder = table.unbuild();
Views.Builder viewsBuilder = keyspace.views.unbuild();
newColumns.forEach(c -> addColumn(keyspace, table, c, ifColumnNotExists, tableBuilder, viewsBuilder));
@ -393,7 +392,6 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
{
Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state);
TableMetadata.Builder builder = table.unbuild();
removedColumns.forEach(c -> dropColumn(keyspace, table, c, ifColumnExists, builder));
return keyspace.withSwapped(keyspace.tables.withSwapped(builder.build()));
@ -461,7 +459,6 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
{
Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state);
TableMetadata.Builder tableBuilder = table.unbuild();
Views.Builder viewsBuilder = keyspace.views.unbuild();
renamedColumns.forEach((o, n) -> renameColumn(keyspace, table, o, n, ifColumnsExists, tableBuilder, viewsBuilder));

View File

@ -157,15 +157,6 @@ public final class Guardrails implements GuardrailsMBean
state -> CONFIG_PROVIDER.getOrCreate(state).getGroupByEnabled(),
"GROUP BY functionality");
/**
* Guardrail disabling ALTER TABLE column mutation access.
*/
public static final EnableFlag alterTableEnabled =
new EnableFlag("alter_table",
null,
state -> CONFIG_PROVIDER.getOrCreate(state).getAlterTableEnabled(),
"User access to ALTER TABLE statement for column mutation");
/**
* Guardrail disabling DROP / TRUNCATE TABLE behavior
*/
@ -184,6 +175,24 @@ public final class Guardrails implements GuardrailsMBean
state -> CONFIG_PROVIDER.getOrCreate(state).getDropKeyspaceEnabled(),
"DROP KEYSPACE functionality");
/**
* Guardrail disabling DDL statements
*/
public static final EnableFlag ddlEnabled =
new EnableFlag("ddl_enabled",
null,
state -> CONFIG_PROVIDER.getOrCreate(state).getDDLEnabled(),
"DDL statement");
/**
* Guardrail disabling DCL statements
*/
public static final EnableFlag dclEnabled =
new EnableFlag("dcl_enabled",
null,
state -> CONFIG_PROVIDER.getOrCreate(state).getDCLEnabled(),
"DCL statement");
/**
* Guardrail disabling user's ability to turn off compression
*/
@ -762,18 +771,6 @@ public final class Guardrails implements GuardrailsMBean
DEFAULT_CONFIG.setUserTimestampsEnabled(enabled);
}
@Override
public boolean getAlterTableEnabled()
{
return DEFAULT_CONFIG.getAlterTableEnabled();
}
@Override
public void setAlterTableEnabled(boolean enabled)
{
DEFAULT_CONFIG.setAlterTableEnabled(enabled);
}
@Override
public boolean getAllowFilteringEnabled()
{
@ -858,6 +855,30 @@ public final class Guardrails implements GuardrailsMBean
DEFAULT_CONFIG.setDropKeyspaceEnabled(enabled);
}
@Override
public boolean getDDLEnabled()
{
return DEFAULT_CONFIG.getDDLEnabled();
}
@Override
public void setDDLEnabled(boolean enabled)
{
DEFAULT_CONFIG.setDDLEnabled(enabled);
}
@Override
public boolean getDCLEnabled()
{
return DEFAULT_CONFIG.getDCLEnabled();
}
@Override
public void setDCLEnabled(boolean enabled)
{
DEFAULT_CONFIG.setDCLEnabled(enabled);
}
@Override
public int getPageSizeWarnThreshold()
{

View File

@ -133,13 +133,6 @@ public interface GuardrailsConfig
*/
boolean getUserTimestampsEnabled();
/**
* Returns whether users are allowed access to the ALTER TABLE statement to mutate columns or not
*
* @return {@code true} if ALTER TABLE ADD/REMOVE/RENAME is allowed, {@code false} otherwise.
*/
boolean getAlterTableEnabled();
/**
* Returns whether tables can be uncompressed
*
@ -175,6 +168,20 @@ public interface GuardrailsConfig
*/
boolean getDropKeyspaceEnabled();
/**
* Returns whether DDL statement is allowed
*
* @return {@code true} if allowed, {@code false} otherwise.
*/
boolean getDDLEnabled();
/**
* Returns whether DCL statement is allowed
*
* @return {@code true} if allowed, {@code false} otherwise.
*/
boolean getDCLEnabled();
/**
* @return The threshold to warn when page size exceeds given size.
*/

View File

@ -264,20 +264,6 @@ public interface GuardrailsMBean
*/
void setCompactTablesEnabled(boolean enabled);
/**
* Gets whether users can use the ALTER TABLE statement to change columns
*
* @return {@code true} if ALTER TABLE is allowed, {@code false} otherwise.
*/
boolean getAlterTableEnabled();
/**
* Sets whether users can use the ALTER TABLE statement to change columns
*
* @param enabled {@code true} if changing columns is allowed, {@code false} otherwise.
*/
void setAlterTableEnabled(boolean enabled);
/**
* Returns whether GROUP BY queries are allowed.
*
@ -316,6 +302,30 @@ public interface GuardrailsMBean
*/
void setDropKeyspaceEnabled(boolean enabled);
/**
* Returns whether DDL statement is allowed
*
* @return {@code true} if allowed, {@code false} otherwise.
*/
boolean getDDLEnabled();
/**
* Sets whether DDL statement is allowed
*/
void setDDLEnabled(boolean enabled);
/**
* Returns whether DCL statement is allowed
*
* @return {@code true} if allowed, {@code false} otherwise.
*/
boolean getDCLEnabled();
/**
* Sets whether DCL statement is allowed
*/
void setDCLEnabled(boolean enabled);
/**
* @return The threshold to warn when requested page size greater than threshold.
* -1 means disabled.

View File

@ -52,7 +52,8 @@ roles_validity: "org.apache.cassandra.config.DurationSpec.IntMillisecondsBound"
coordinator_read_size_warn_threshold: "org.apache.cassandra.config.DataStorageSpec.LongBytesBound"
scripted_user_defined_functions_enabled: "java.lang.Boolean"
auth_cache_warming_enabled: "java.lang.Boolean"
alter_table_enabled: "java.lang.Boolean"
ddl_enabled: "java.lang.Boolean"
dcl_enabled: "java.lang.Boolean"
client_request_size_metrics_enabled: "java.lang.Boolean"
cdc_on_repair_enabled: "java.lang.Boolean"
entire_sstable_stream_throughput_outbound: "org.apache.cassandra.config.DataRateSpec.LongBytesPerSecondBound"

View File

@ -22,25 +22,33 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests the guardrail for disabling user access to the ALTER TABLE statement, {@link Guardrails#alterTableEnabled}.
*
* Tests the guardrail for disabling user access to the ALTER TABLE statement, {@link Guardrails#ddlEnabled}.
* <p>
* NOTE: This test class depends on {@link #currentTable()} method for setup, cleanup, and execution of tests. You'll
* need to refactor this if you add tests that make changes to the current table as the test teardown will no longer match
* setup.
* <p>
* This test is a leftover from CASSANDRA-17495 which was accommodated for the purposes of CASSANDRA-19556.
*/
public class GuardrailAlterTableTest extends GuardrailTester
{
private static final String DDL_ERROR_MSG = "DDL statement is not allowed";
public GuardrailAlterTableTest()
{
super(Guardrails.alterTableEnabled);
super(Guardrails.ddlEnabled);
}
@Before
public void setupTest() throws Throwable
{
createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, PRIMARY KEY(k, c))");
super.beforeGuardrailTest();
}
@After
@ -52,82 +60,60 @@ public class GuardrailAlterTableTest extends GuardrailTester
private void setGuardrail(boolean alterTableEnabled)
{
guardrails().setAlterTableEnabled(alterTableEnabled);
guardrails().setDDLEnabled(alterTableEnabled);
}
/**
* Confirm that ALTER TABLE queries either work (guardrail enabled) or fail (guardrail disabled) appropriately
* @throws Throwable
*/
@Test
public void testGuardrailEnabledAndDisabled() throws Throwable
public void testGuardrailEnabledAndDisabled()
{
setGuardrail(false);
assertFails("ALTER TABLE %s ADD test_one text;", "changing columns");
assertFailQuery("ALTER TABLE %s ADD test_one text");
setGuardrail(true);
assertValid("ALTER TABLE %s ADD test_two text;");
executeNet("ALTER TABLE %s ADD test_two text;");
setGuardrail(false);
assertFails("ALTER TABLE %s ADD test_three text;", "changing columns");
assertFailQuery("ALTER TABLE %s ADD test_three text");
}
/**
* Confirm the guardrail appropriately catches the ALTER DROP case on a column
* @throws Throwable
*/
@Test
public void testAppliesToAlterDropColumn() throws Throwable
public void testAppliesToAlterDropColumn()
{
setGuardrail(true);
assertValid("ALTER TABLE %s ADD test_one text;");
executeNet("ALTER TABLE %s ADD test_one text;");
setGuardrail(false);
assertFails("ALTER TABLE %s DROP test_one", "changing columns");
assertFailQuery("ALTER TABLE %s DROP test_one");
setGuardrail(true);
assertValid("ALTER TABLE %s DROP test_one");
executeNet("ALTER TABLE %s DROP test_one");
}
/**
* Confirm the guardrail appropriately catches the ALTER RENAME case on a column
* @throws Throwable
*/
@Test
public void testAppliesToAlterRenameColumn() throws Throwable
public void testAppliesToAlterRenameColumn()
{
setGuardrail(false);
assertFails("ALTER TABLE %s RENAME c TO renamed_c", "changing columns");
setGuardrail(false);
assertFailQuery("ALTER TABLE %s RENAME c TO renamed_c");
setGuardrail(true);
assertValid("ALTER TABLE %s RENAME c TO renamed_c");
executeNet("ALTER TABLE %s RENAME c TO renamed_c");
}
/**
* Confirm we can always alter properties via the options map regardless of guardrail state
* @throws Throwable
*/
@Test
public void testAlterViaMapAlwaysWorks() throws Throwable
private void assertFailQuery(String query)
{
setGuardrail(false);
assertValid("ALTER TABLE %s WITH compression = { 'class' : 'SnappyCompressor', 'chunk_length_in_kb' : 32 };");
setGuardrail(true);
assertValid("ALTER TABLE %s WITH compression = { 'class' : 'SnappyCompressor', 'chunk_length_in_kb' : 32 };");
}
/**
* Confirm the other form of ALTER TABLE property map changing always works regardless of guardrail state
* @throws Throwable
*/
@Test
public void testAlterOptionsAlwaysWorks() throws Throwable
{
setGuardrail(true);
assertValid("ALTER TABLE %s WITH GC_GRACE_SECONDS = 456; ");
setGuardrail(false);
assertValid("ALTER TABLE %s WITH GC_GRACE_SECONDS = 123; ");
assertThatThrownBy(() -> executeNet(query))
.isInstanceOf(InvalidQueryException.class)
.hasMessageContaining(DDL_ERROR_MSG);
}
}

View File

@ -0,0 +1,210 @@
/*
* 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.db.guardrails;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.statements.AuthenticationStatement;
import org.apache.cassandra.cql3.statements.AuthorizationStatement;
import org.apache.cassandra.service.QueryState;
import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class GuardrailDCLEnabledTest extends GuardrailTester
{
private static final String DCL_ERROR_MSG = "DCL statement is not allowed";
private static final String DCL_TEST_NEW_USER = "dcltest";
private void setGuardrail(boolean enabled)
{
Guardrails.instance.setDCLEnabled(enabled);
}
@Before
public void beforeGuardrailTest() throws Throwable
{
super.beforeGuardrailTest();
useSuperUser();
// need permission on roles to test dcl queries
executeNet(format("GRANT ALL ON ALL ROLES TO %s", USERNAME));
useUser(USERNAME, PASSWORD);
createTable(KEYSPACE, "CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, PRIMARY KEY(k, c))");
}
@After
public void afterTest()
{
setGuardrail(true);
executeNet(dropRole(DCL_TEST_NEW_USER));
dropTable("DROP TABLE IF EXISTS %s");
}
@Test
public void testCannotCreateRoleWhileFeatureDisabled() throws Throwable
{
setGuardrail(false);
shouldFailWithDCLErrorMsg(getCreateRoleCQL(DCL_TEST_NEW_USER));
// no role is created
assertEmpty(execute(format("SELECT * FROM system_auth.roles WHERE role='%s'",
DCL_TEST_NEW_USER)));
setGuardrail(true);
executeNet(getCreateRoleCQL(DCL_TEST_NEW_USER));
// role is created
assertRowCount(execute(format("SELECT * FROM system_auth.roles WHERE role='%s'",
DCL_TEST_NEW_USER)),
1);
}
@Test
public void testCannotDropRoleWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateRoleCQL(DCL_TEST_NEW_USER));
setGuardrail(false);
shouldFailWithDCLErrorMsg(dropRole(DCL_TEST_NEW_USER));
// role is not dropped
assertRowCount(execute(format("SELECT * FROM system_auth.roles WHERE role='%s'",
DCL_TEST_NEW_USER)),
1);
setGuardrail(true);
executeNet(dropRole(DCL_TEST_NEW_USER));
// role is dropped
assertEmpty(execute(format("SELECT * FROM system_auth.roles WHERE role='%s'",
DCL_TEST_NEW_USER)));
}
@Test
public void testCannotListRoleWhileFeatureDisabled()
{
setGuardrail(false);
shouldFailWithDCLErrorMsg("LIST ROLES");
shouldFailWithDCLErrorMsg(format("LIST ALL PERMISSIONS OF %s", USERNAME));
shouldFailWithDCLErrorMsg("LIST USERS");
setGuardrail(true);
executeNet("LIST ROLES");
executeNet(format("LIST ALL PERMISSIONS OF %s", USERNAME));
executeNet("LIST USERS");
}
@Test
public void testCannotGrantPermissionWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateRoleCQL(DCL_TEST_NEW_USER));
setGuardrail(false);
shouldFailWithDCLErrorMsg(getGrantPermissionCQL(DCL_TEST_NEW_USER, KEYSPACE, currentTable()));
// DCL_TEST_NEW_USER doesn't get permission
assertEmpty(execute(format("SELECT * FROM system_auth.role_permissions WHERE role='%s' AND resource='data/%s/%s'",
DCL_TEST_NEW_USER, KEYSPACE, currentTable())));
setGuardrail(true);
executeNet(getGrantPermissionCQL(DCL_TEST_NEW_USER, KEYSPACE, currentTable()));
assertRowCount(execute(format("SELECT * FROM system_auth.role_permissions WHERE role='%s' AND resource='data/%s/%s'",
DCL_TEST_NEW_USER, KEYSPACE, currentTable())),
1);
}
@Test
public void testCannotRevokePermissionWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateRoleCQL(DCL_TEST_NEW_USER));
executeNet(getGrantPermissionCQL(DCL_TEST_NEW_USER, KEYSPACE, currentTable()));
setGuardrail(false);
shouldFailWithDCLErrorMsg(getRevokePermissionCQL(DCL_TEST_NEW_USER, KEYSPACE, currentTable()));
// DCL_TEST_NEW_USER permission wasn't revoked on KEYSPACE
assertRowCount(execute(format("SELECT * FROM system_auth.role_permissions WHERE role='%s' AND resource='data/%s/%s'",
DCL_TEST_NEW_USER, KEYSPACE, currentTable())),
1);
setGuardrail(true);
executeNet(getRevokePermissionCQL(DCL_TEST_NEW_USER, KEYSPACE, currentTable()));
assertEmpty(execute(format("SELECT * FROM system_auth.role_permissions WHERE role='%s' AND resource='data/%s/%s'",
DCL_TEST_NEW_USER, KEYSPACE, currentTable())));
}
@Test
public void testMockDCLStatementsWhileFeaturesDisabled()
{
AuthorizationStatement authorizationStatement = mock(AuthorizationStatement.class);
AuthenticationStatement authenticationStatement = mock(AuthenticationStatement.class);
QueryState queryState = mock(QueryState.class);
doReturn(userClientState).when(queryState).getClientState();
doCallRealMethod().when(authorizationStatement).isDCLStatement();
doCallRealMethod().when(authenticationStatement).isDCLStatement();
QueryProcessor qp = QueryProcessor.instance;
setGuardrail(false);
assertThatThrownBy(() -> qp.processStatement(authorizationStatement, queryState, QueryOptions.DEFAULT, 0L))
.isInstanceOf(GuardrailViolatedException.class);
verify(authorizationStatement).isDCLStatement();
assertThatThrownBy(() -> qp.processStatement(authenticationStatement, queryState, QueryOptions.DEFAULT, 0L))
.isInstanceOf(GuardrailViolatedException.class);
verify(authenticationStatement).isDCLStatement();
setGuardrail(true);
qp.processStatement(authorizationStatement, queryState, QueryOptions.DEFAULT, 0L);
qp.processStatement(authenticationStatement, queryState, QueryOptions.DEFAULT, 0L);
}
private void shouldFailWithDCLErrorMsg(String query)
{
assertThatThrownBy(() -> executeNet(query))
.isInstanceOf(InvalidQueryException.class)
.hasMessageContaining(DCL_ERROR_MSG);
}
private static String getCreateRoleCQL(String role)
{
return format("CREATE ROLE IF NOT EXISTS %s WITH PASSWORD = 'test'",
role);
}
private static String getGrantPermissionCQL(String role, String ks, String tbl)
{
return format("GRANT ALL PERMISSIONS ON %s.%s TO %s;", ks, tbl, role);
}
private static String getRevokePermissionCQL(String role, String ks, String tbl)
{
return format("REVOKE ALL ON %s.%s FROM %s;", ks, tbl, role);
}
private static String dropRole(String role)
{
return format("DROP ROLE IF EXISTS %s", role);
}
}

View File

@ -0,0 +1,338 @@
/*
* 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.db.guardrails;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.statements.schema.AlterSchemaStatement;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.SchemaKeyspaceTables;
import org.apache.cassandra.service.QueryState;
import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class GuardrailDDLEnabledTest extends GuardrailTester
{
private static final String TEST_KS = "ddlks";
private static final String TEST_TABLE = "ddltbl";
private static final String TEST_VIEW = "ddlview";
private static final String DDL_ERROR_MSG = "DDL statement is not allowed";
private void setGuardrail(boolean enabled)
{
Guardrails.instance.setDDLEnabled(enabled);
}
@Before
public void beforeGuardrailTest() throws Throwable
{
super.beforeGuardrailTest();
// grant current user permission to all keyspaces
useSuperUser();
executeNet(format("GRANT ALL ON ALL KEYSPACES TO %s", USERNAME));
useUser(USERNAME, PASSWORD);
}
@After
public void afterTest()
{
setGuardrail(true);
executeNet(getDropViewCQL(TEST_KS, TEST_VIEW));
executeNet(getDropKeyspaceCQL(TEST_KS));
}
@Test
public void testCannotCreateKeyspaceWhileFeatureDisabled() throws Throwable
{
setGuardrail(false);
shouldFailWithDDLErrorMsg(getCreateKeyspaceCQL(TEST_KS));
// No new keyspace should be created
assertEmpty(execute(getSystemSchemaKeyspaceCQL(TEST_KS)));
setGuardrail(true);
executeNet(getCreateKeyspaceCQL(TEST_KS));
assertRowCount(execute(getSystemSchemaKeyspaceCQL(TEST_KS)), 1);
}
@Test
public void testCannotCreateTableWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
setGuardrail(false);
shouldFailWithDDLErrorMsg(getCreateTableCQL(TEST_KS, TEST_TABLE));
// No new table should be created
assertEmpty(execute(getSystemSchemaTableCQL(TEST_KS, TEST_TABLE)));
setGuardrail(true);
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
assertRowCount(execute(getSystemSchemaTableCQL(TEST_KS, TEST_TABLE)), 1);
}
@Test
public void testCannotCreateViewWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
setGuardrail(false);
shouldFailWithDDLErrorMsg(getCreateViewCQL(TEST_KS, TEST_VIEW));
// No new view should be created
assertEmpty(execute(getSystemSchemaViewCQL(TEST_KS, TEST_VIEW)));
setGuardrail(true);
executeNet(getCreateViewCQL(TEST_KS, TEST_VIEW));
assertRowCount(execute(getSystemSchemaViewCQL(TEST_KS, TEST_VIEW)), 1);
}
@Test
public void testCannotDropKeyspaceWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
setGuardrail(false);
shouldFailWithDDLErrorMsg(getDropKeyspaceCQL(TEST_KS));
assertRowCount(execute(getSystemSchemaKeyspaceCQL(TEST_KS)), 1);
setGuardrail(true);
executeNet(getDropKeyspaceCQL(TEST_KS));
assertEmpty(execute(getSystemSchemaKeyspaceCQL(TEST_KS)));
}
@Test
public void testCannotDropTableWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
setGuardrail(false);
shouldFailWithDDLErrorMsg(getDropTableCQL(TEST_KS, TEST_TABLE));
assertRowCount(execute(getSystemSchemaTableCQL(TEST_KS, TEST_TABLE)), 1);
setGuardrail(true);
executeNet(getDropTableCQL(TEST_KS, TEST_TABLE));
assertEmpty(execute(getSystemSchemaTableCQL(TEST_KS, TEST_TABLE)));
}
@Test
public void testCannotDropViewWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
executeNet(getCreateViewCQL(TEST_KS, TEST_VIEW));
setGuardrail(false);
shouldFailWithDDLErrorMsg(getDropViewCQL(TEST_KS, TEST_VIEW));
assertRowCount(execute(getSystemSchemaViewCQL(TEST_KS, TEST_VIEW)), 1);
setGuardrail(true);
executeNet(getDropViewCQL(TEST_KS, TEST_VIEW));
assertEmpty(execute(getSystemSchemaViewCQL(TEST_KS, TEST_VIEW)));
}
@Test
public void testCannotDropColumnWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
setGuardrail(false);
// table have column col1
assertRowCount(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND column_name='col1'",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.COLUMNS,
TEST_KS,
TEST_TABLE)), 1);
shouldFailWithDDLErrorMsg(format("ALTER TABLE %s.%s DROP IF EXISTS col1", TEST_KS, TEST_TABLE));
// column col1 should not be dropped
assertRowCount(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND column_name='col1'",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.COLUMNS,
TEST_KS,
TEST_TABLE)), 1);
setGuardrail(true);
executeNet(format("ALTER TABLE %s.%s DROP IF EXISTS col1", TEST_KS, TEST_TABLE));
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND column_name='col1'",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.COLUMNS,
TEST_KS,
TEST_TABLE)));
}
@Test
public void testCannotAlterKeyspaceWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
setGuardrail(false);
// keyspace should have durable_write=true by default
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND durable_writes=false ALLOW FILTERING",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.KEYSPACES,
TEST_KS)));
shouldFailWithDDLErrorMsg(format("ALTER KEYSPACE %s WITH durable_writes=false", TEST_KS));
// keyspace should still have durable_write=true
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND durable_writes=false ALLOW FILTERING",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.KEYSPACES,
TEST_KS)));
setGuardrail(true);
executeNet(format("ALTER KEYSPACE %s WITH durable_writes=false", TEST_KS));
assertRowCount(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND durable_writes=false ALLOW FILTERING",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.KEYSPACES,
TEST_KS)), 1);
}
@Test
public void testCannotAlterTableWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
setGuardrail(false);
// table doesn't have comment
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND comment='test' ALLOW FILTERING",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.TABLES,
TEST_KS, TEST_TABLE)));
shouldFailWithDDLErrorMsg(format("ALTER TABLE %s.%s WITH comment='test'", TEST_KS, TEST_TABLE));
// table should not have comment
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND comment='test' ALLOW FILTERING",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.TABLES,
TEST_KS, TEST_TABLE)));
setGuardrail(true);
executeNet(format("ALTER TABLE %s.%s WITH comment='test'", TEST_KS, TEST_TABLE));
assertRowCount(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND comment='test' ALLOW FILTERING",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.TABLES,
TEST_KS, TEST_TABLE)), 1);
}
@Test
public void testCannotAddColumnWhileFeatureDisabled() throws Throwable
{
executeNet(getCreateKeyspaceCQL(TEST_KS));
executeNet(getCreateTableCQL(TEST_KS, TEST_TABLE));
setGuardrail(false);
// table doesn't have new column col3
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND column_name='col3'",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.COLUMNS,
TEST_KS, TEST_TABLE)));
shouldFailWithDDLErrorMsg(format("ALTER TABLE %s.%s ADD col3 text", TEST_KS, TEST_TABLE));
// table should not have new column col3
assertEmpty(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND column_name='col3'",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.COLUMNS,
TEST_KS, TEST_TABLE)));
setGuardrail(true);
executeNet(format("ALTER TABLE %s.%s ADD col3 text", TEST_KS, TEST_TABLE));
// table should not have new column col3
assertRowCount(execute(format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND column_name='col3'",
SchemaConstants.SCHEMA_KEYSPACE_NAME,
SchemaKeyspaceTables.COLUMNS,
TEST_KS, TEST_TABLE)), 1);
}
@Test
public void testMockAlterSchemaStatementsWhileFeaturesDisabled()
{
AlterSchemaStatement alterSchemaStatement = mock(AlterSchemaStatement.class);
QueryState queryState = mock(QueryState.class);
doReturn(userClientState).when(queryState).getClientState();
doCallRealMethod().when(alterSchemaStatement).isDDLStatement();
QueryProcessor qp = QueryProcessor.instance;
setGuardrail(false);
assertThatThrownBy(() -> qp.processStatement(alterSchemaStatement, queryState, QueryOptions.DEFAULT, 0L))
.isInstanceOf(GuardrailViolatedException.class);
verify(alterSchemaStatement).isDDLStatement();
setGuardrail(true);
qp.processStatement(alterSchemaStatement, queryState, QueryOptions.DEFAULT, 0L);
}
private void shouldFailWithDDLErrorMsg(String query)
{
assertThatThrownBy(() -> executeNet(query))
.isInstanceOf(InvalidQueryException.class)
.hasMessageContaining(DDL_ERROR_MSG);
}
private String getCreateKeyspaceCQL(String ks)
{
return format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", ks);
}
private String getDropKeyspaceCQL(String ks)
{
return format("DROP KEYSPACE IF EXISTS %s", ks);
}
private String getCreateTableCQL(String ks, String table)
{
return format("CREATE TABLE IF NOT EXISTS %s.%s (key text PRIMARY KEY, col1 text, col2 text)", ks, table);
}
private String getDropTableCQL(String ks, String table)
{
return format("DROP TABLE IF EXISTS %s.%s", ks, table);
}
private String getCreateViewCQL(String ks, String table)
{
return format("CREATE MATERIALIZED VIEW IF NOT EXISTS %s.%s AS SELECT key FROM %s.%s WHERE key IS NOT NULL PRIMARY KEY (key)", ks, table, TEST_KS, TEST_TABLE);
}
private String getDropViewCQL(String ks, String view)
{
return format("DROP MATERIALIZED VIEW IF EXISTS %s.%s", ks, view);
}
private String getSystemSchemaKeyspaceCQL(String ks)
{
return format("SELECT * FROM %s.%s WHERE keyspace_name='%s'", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.KEYSPACES, ks);
}
private String getSystemSchemaTableCQL(String ks, String table)
{
return format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s'", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.TABLES, ks, table);
}
private String getSystemSchemaViewCQL(String ks, String view)
{
return format("SELECT * FROM %s.%s WHERE keyspace_name='%s' AND view_name='%s'", SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.VIEWS, ks, view);
}
}

View File

@ -86,8 +86,8 @@ public abstract class GuardrailTester extends CQLTester
// previously created table, which is not what we want).
protected static final String FAIL_TABLE = "abort_table_creation_test";
private static final String USERNAME = "guardrail_user";
private static final String PASSWORD = "guardrail_password";
protected static final String USERNAME = "guardrail_user";
protected static final String PASSWORD = "guardrail_password";
protected static ClientState systemClientState, userClientState, superClientState;