blob: 3b26a568570eae3ef6bd73526dca13d80fd31cfb [file] [log] [blame]
/*
* 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.functions.masking;
import org.junit.Test;
import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.Session;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.assertj.core.api.Assertions;
import static java.lang.String.format;
import static org.junit.Assert.assertNull;
/**
* {@link ColumnMaskTester} for column masks using a UDF.
*/
public class ColumnMaskWithUDFTest extends ColumnMaskTester
{
@Test
@SuppressWarnings("resource")
public void testUDF() throws Throwable
{
// create a table masked with a UDF and with a materialized view
String function = createAddFunction();
createTable(format("CREATE TABLE %%s (k int, c int, v int MASKED WITH %s(7), PRIMARY KEY (k, c))", function));
String view = createView("CREATE MATERIALIZED VIEW %s AS SELECT * FROM %s " +
"WHERE k IS NOT NULL AND c IS NOT NULL AND v IS NOT NULL " +
"PRIMARY KEY (v, k, c)");
// add some data
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 10)");
execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 20)");
Object[][] clearRows = rows(row(0, 0, 10), row(0, 1, 20));
Object[][] maskedRows = rows(row(0, 0, 17), row(0, 1, 27));
waitForViewMutations();
Session session = sessionNet();
// query the table and verify that the column mask is applied
String tableQuery = "SELECT k, c, v FROM %s";
assertRowsNet(executeNet(tableQuery), maskedRows);
BoundStatement tablePrepared = session.prepare(formatQuery(tableQuery)).bind();
assertRowsNet(session.execute(tablePrepared), maskedRows);
// query the view and verify that the column mask is applied
String viewQuery = format("SELECT k, c, v FROM %s.%s", KEYSPACE, view);
BoundStatement viewPrepared = session.prepare(formatQuery(tableQuery)).bind();
assertRowsNet(executeNet(viewQuery), maskedRows);
assertRowsNet(session.execute(viewPrepared), maskedRows);
// drop the column mask and verify that the column is not masked anymore
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
assertRowsNet(executeNet(tableQuery), clearRows);
assertRowsNet(executeNet(viewQuery), clearRows);
assertRowsNet(session.execute(tablePrepared), clearRows);
assertRowsNet(session.execute(viewPrepared), clearRows);
// mask the column again, but this time with a different argument
alterTable(format("ALTER TABLE %%s ALTER v MASKED WITH %s(8)", function));
maskedRows = rows(row(0, 0, 18), row(0, 1, 28));
assertRowsNet(executeNet(tableQuery), maskedRows);
assertRowsNet(executeNet(viewQuery), maskedRows);
assertRowsNet(session.execute(tablePrepared), maskedRows);
assertRowsNet(session.execute(viewPrepared), maskedRows);
}
@Test
public void testUDFWithReversed() throws Throwable
{
// create a table masked with a UDF and with a materialized view
String function = createAddFunction();
createTable(format("CREATE TABLE %%s (k int, c int MASKED WITH %s(100), v int, PRIMARY KEY (k, c)) " +
"WITH CLUSTERING ORDER BY (c DESC)", function));
String view = createView("CREATE MATERIALIZED VIEW %s AS SELECT * FROM %s " +
"WHERE k IS NOT NULL AND c IS NOT NULL AND v IS NOT NULL " +
"PRIMARY KEY (v, c, K) " +
"WITH CLUSTERING ORDER BY (c DESC, k ASC)");
// add some data
execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 0)");
execute("INSERT INTO %s (k, c, v) VALUES (0, 2, 0)");
Object[][] clearRows = rows(row(0, 2, 0), row(0, 1, 0));
Object[][] maskedRows = rows(row(0, 102, 0), row(0, 101, 0));
waitForViewMutations();
// query the table and verify that the column mask is applied
String tableQuery = "SELECT k, c, v FROM %s";
assertRowsNet(executeNet(tableQuery), maskedRows);
// query the view and verify that the column mask is applied
String viewQuery = format("SELECT k, c, v FROM %s.%s", KEYSPACE, view);
assertRowsNet(executeNet(viewQuery), maskedRows);
// drop the column mask and verify that the column is not masked anymore
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
assertRowsNet(executeNet(tableQuery), clearRows);
assertRowsNet(executeNet(viewQuery), clearRows);
// mask the column again, but this time with a different argument
alterTable(format("ALTER TABLE %%s ALTER c MASKED WITH %s(1000)", function));
maskedRows = rows(row(0, 1002, 0), row(0, 1001, 0));
assertRowsNet(executeNet(tableQuery), maskedRows);
assertRowsNet(executeNet(viewQuery), maskedRows);
}
/**
* Verifies that queries dropping a UDF that is used for masking columns are rejected.
*/
@Test
public void testDropUDFWithDependentMasks() throws Throwable
{
String function = createAddFunction();
String mask = format("MASKED WITH %s(7)", function);
String table1 = createTable(format("CREATE TABLE %%s (k int %s PRIMARY KEY, v int %<s)", mask));
String table2 = createTable(format("CREATE TABLE %%s (k int %s, c int %<s, v int %<s, s int static %<s, PRIMARY KEY (k, c))", mask));
String dropFunctionQuery = format("DROP FUNCTION %s", function);
String message = format("Function '%s' is still referenced by column masks in tables %s, %s", function, table1, table2);
assertInvalidThrowMessage(message, InvalidRequestException.class, dropFunctionQuery);
alterTable(format("ALTER TABLE %s.%s ALTER k DROP MASKED", KEYSPACE, table1));
alterTable(format("ALTER TABLE %s.%s ALTER k DROP MASKED", KEYSPACE, table2));
assertInvalidThrowMessage(message, InvalidRequestException.class, dropFunctionQuery);
alterTable(format("ALTER TABLE %s.%s ALTER v DROP MASKED", KEYSPACE, table1));
alterTable(format("ALTER TABLE %s.%s ALTER v DROP MASKED", KEYSPACE, table2));
message = format("Function '%s' is still referenced by column masks in tables %s", function, table2);
assertInvalidThrowMessage(message, InvalidRequestException.class, dropFunctionQuery);
alterTable(format("ALTER TABLE %s.%s ALTER c DROP MASKED", KEYSPACE, table2));
assertInvalidThrowMessage(message, InvalidRequestException.class, dropFunctionQuery);
alterTable(format("ALTER TABLE %s.%s ALTER s DROP MASKED", KEYSPACE, table2));
schemaChange(dropFunctionQuery);
}
@Test
public void testMissingUDF() throws Throwable
{
assertMaskingFails("Unable to find masking function for v, no declared function matches the signature %s()",
"missing_udf");
}
@Test
public void testUDFWithNoArguments() throws Throwable
{
assertMaskingFails("Invalid number of arguments in call to function %s: 0 required but 1 provided",
createFunction(KEYSPACE,
"",
"CREATE FUNCTION %s () " +
"CALLED ON NULL INPUT " +
"RETURNS int " +
"LANGUAGE java " +
"AS 'return 42;'"));
}
@Test
public void testUDFWithWrongArgumentCount() throws Throwable
{
assertMaskingFails("Invalid number of arguments in call to function %s: 2 required but 3 provided",
createAddFunction(),
"(1, 3)");
}
@Test
public void testUDFWithWrongArgumentType() throws Throwable
{
assertMaskingFails("Type error: org.apache.cassandra.db.marshal.Int32Type " +
"cannot be passed as argument 0 of function %s of type text",
createFunction(KEYSPACE,
"text",
"CREATE FUNCTION %s (a text) " +
"CALLED ON NULL INPUT " +
"RETURNS text " +
"LANGUAGE java " +
"AS 'return \"redacted\";'"));
}
@Test
public void testUDFWithWrongReturnType() throws Throwable
{
assertMaskingFails("Masking function %s() return type is text",
createFunction(KEYSPACE,
"int",
"CREATE FUNCTION %s (a int) " +
"CALLED ON NULL INPUT " +
"RETURNS text " +
"LANGUAGE java " +
"AS 'return \"redacted\";'"));
}
@Test
public void testUDFInOtherKeyspace() throws Throwable
{
assertMaskingFails("Masking function %s() doesn't belong to the same keyspace as the table",
createFunction(KEYSPACE_PER_TEST,
"int",
"CREATE FUNCTION %s (input int) " +
"CALLED ON NULL INPUT " +
"RETURNS int " +
"LANGUAGE java " +
"AS 'return Integer.valueOf(input);'"));
}
@Test
public void testUDA() throws Throwable
{
String fState = createAddFunction();
String fFinal = createIdentityFunction();
assertMaskingFails("Aggregate function %s() cannot be used for masking table columns",
createAggregate(KEYSPACE,
"int",
"CREATE AGGREGATE %s(int) " +
"SFUNC " + shortFunctionName(fState) + " " +
"STYPE int " +
"FINALFUNC " + shortFunctionName(fFinal) + " " +
"INITCOND 42"));
}
private void assertMaskingFails(String message, String function) throws Throwable
{
assertMaskingFails(message, function, "()");
}
private void assertMaskingFails(String message, String function, String arguments) throws Throwable
{
message = format(message, function);
// create table should fail
Assertions.assertThatThrownBy(() -> execute(format("CREATE TABLE %s.%s (k int PRIMARY KEY, v int MASKED WITH %s%s)",
KEYSPACE, createTableName(), function, arguments)))
.isInstanceOf(InvalidRequestException.class)
.hasMessageContaining(message);
assertNull(currentTableMetadata());
// alter table should fail
String table = createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
Assertions.assertThatThrownBy(() -> execute(format("ALTER TABLE %s.%s ALTER v MASKED WITH %s%s",
KEYSPACE, table, function, arguments)))
.isInstanceOf(InvalidRequestException.class)
.hasMessageContaining(message);
assertColumnIsNotMasked(table, "v");
}
private String createAddFunction() throws Throwable
{
return createFunction(KEYSPACE,
"int, int",
"CREATE FUNCTION %s (a int, b int) " +
"CALLED ON NULL INPUT " +
"RETURNS int " +
"LANGUAGE java " +
"AS 'return Integer.valueOf(a) + Integer.valueOf(b);'");
}
private String createIdentityFunction() throws Throwable
{
return createFunction(KEYSPACE,
"int, int",
"CREATE FUNCTION %s (a int) " +
"CALLED ON NULL INPUT " +
"RETURNS int " +
"LANGUAGE java " +
"AS 'return a;'");
}
}