mirror of https://github.com/apache/cassandra
CEP-20: Dynamic Data Masking
Allows to attach the native masking functions and UDFs to the definitions of table columns in the schema, as defined by CEP-20. The functions masking a column can be specified on CREATE TABLE queries, right at the end of the column definition. The mask of a column can also be changed or dropped with an ALTER TABLE query. Once a column is masked, SELECT queries will always return the masked value of the column. That masking is done on the coordinator, at the end of the query execution. Thus, masking won't affect any filters or ordering, which would be based on the clear values of the masked columns. Column masks are stored on the table system_schema.column_masks. A new UNMASK permission allows to see the clear data of columns with an attached mask. Also, a new SELECT_MASKED permission allows to run SELECT queries restricting the clear values of masked columns. Superusers have both permissions by default, whereas regular users don't have them. Dynamic data masking can be enabled/disabled with the config property dynamic_data_masking_enabled in cassandra.yaml. It is disabled by default. This is the combination of multiple tickets: * Add masking functions to column metadata (CASSANDRA-18068) * Add UNMASK permission (CASSANDRA-18069) * Add SELECT_MASKED permission (CASSANDRA-18070) * Add support for using UDFs as masking functions (CASSANDRA-18071) * Add feature flag for dynamic data masking (CASSANDRA-18316) patch by Andrés de la Peña; reviewed by Benjamin Lerer and Berenguer Blasi for CASSANDRA-17940
This commit is contained in:
parent
de1ff6d4d1
commit
2e2a49fcdc
|
|
@ -1,4 +1,5 @@
|
|||
5.0
|
||||
* CEP-20: Dynamic Data Masking (CASSANDRA-17940)
|
||||
* Add system_views.snapshots virtual table (CASSANDRA-18102)
|
||||
* Update OpenHFT dependencies (chronicle-queue, chronicle-core, chronicle-bytes, chronicle-wire, chronicle-threads) (CASSANDRA-18049)
|
||||
* Remove org.apache.cassandra.hadoop code (CASSANDRA-18323)
|
||||
|
|
|
|||
8
NEWS.txt
8
NEWS.txt
|
|
@ -132,6 +132,14 @@ New features
|
|||
- Added local read/write ratio to tablestats.
|
||||
- Added system_views.max_sstable_size and system_views.max_sstable_duration tables.
|
||||
- Added virtual table system_views.snapshots to see all snapshots from CQL shell.
|
||||
- Added support for attaching CQL dynamic data masking functions to table columns on the schema. These masking
|
||||
functions can be attached to or dettached from columns with CREATE/ALTER TABLE statements. The functions obscure
|
||||
the masked data during queries, but they don't change the stored data.
|
||||
- Added new UNMASK permission. It allows to see the clear data of columns with an attached mask. Superusers have it
|
||||
by default, whereas regular users don't have it by default.
|
||||
- Added new SELECT_MASKED permission. It allows to run SELECT queries selecting the clear values of masked columns.
|
||||
Superusers have it by default, whereas regular users don't have it by default.
|
||||
- Added support for using UDFs as masking functions attached to table columns on the schema.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
|
|
|||
|
|
@ -1660,6 +1660,14 @@ report_unconfirmed_repaired_data_mismatches: false
|
|||
# warming of auth caches prior to node completing startup. See CASSANDRA-16958
|
||||
# auth_cache_warming_enabled: false
|
||||
|
||||
# If enabled, dynamic data masking allows to attach CQL masking functions to the columns of a table.
|
||||
# Users without the UNMASK permission will see an obscured version of the values of the columns with an attached mask.
|
||||
# If dynamic data masking is disabled it won't be allowed to create new column masks, although it will still be possible
|
||||
# to drop any previously existing masks. Also, any existing mask will be ignored at query time, so all users will see
|
||||
# the clear values of the masked columns.
|
||||
# Defaults to false to disable dynamic data masking.
|
||||
# dynamic_data_masking_enabled: false
|
||||
|
||||
#########################
|
||||
# EXPERIMENTAL FEATURES #
|
||||
#########################
|
||||
|
|
|
|||
|
|
@ -252,9 +252,11 @@ bc(syntax)..
|
|||
'(' <column-definition> ( ',' <column-definition> )* ')'
|
||||
( WITH <option> ( AND <option>)* )?
|
||||
|
||||
<column-definition> ::= <identifier> <type> ( STATIC )? ( PRIMARY KEY )?
|
||||
<column-definition> ::= <identifier> <type> ( STATIC )? ( <column_mask> )? ( PRIMARY KEY )?
|
||||
| PRIMARY KEY '(' <partition-key> ( ',' <identifier> )* ')'
|
||||
|
||||
<column-mask> ::= MASKED WITH ( DEFAULT | <function> '(' ( <term> (',' <term>)* )? ')' )
|
||||
|
||||
<partition-key> ::= <identifier>
|
||||
| '(' <identifier> (',' <identifier> )* ')'
|
||||
|
||||
|
|
@ -410,11 +412,15 @@ __Syntax:__
|
|||
bc(syntax)..
|
||||
<alter-table-stmt> ::= ALTER (TABLE | COLUMNFAMILY) (IF NOT EXISTS)? <tablename> <instruction>
|
||||
|
||||
<instruction> ::= ADD (IF NOT EXISTS)? ( <identifier> <type> ( , <identifier> <type> )* )
|
||||
<instruction> ::= ADD (IF NOT EXISTS)? ( <identifier> <type> ( <column-mask> )?
|
||||
( , <identifier> <type> ( <column-mask> )? )* )
|
||||
| DROP (IF EXISTS)? ( <identifier> ( , <identifier> )* )
|
||||
| ALTER <identifier> ( <column-mask> | DROP MASKED )
|
||||
| RENAME (IF EXISTS)? <identifier> to <identifier> (AND <identifier> to <identifier>)*
|
||||
| DROP COMPACT STORAGE
|
||||
| WITH <option> ( AND <option> )*
|
||||
|
||||
<column-mask> ::= MASKED WITH ( DEFAULT | <function> '(' ( <term> (',' <term>)* )? ')' )
|
||||
p.
|
||||
__Sample:__
|
||||
|
||||
|
|
@ -434,6 +440,7 @@ If the table does not exist, the statement will return an error, unless @IF EXIS
|
|||
The @<tablename>@ is the table name optionally preceded by the keyspace name. The @<instruction>@ defines the alteration to perform:
|
||||
* @ADD@: Adds a new column to the table. The @<identifier>@ for the new column must not conflict with an existing column. Moreover, columns cannot be added to tables defined with the @COMPACT STORAGE@ option. If the new column already exists, the statement will return an error, unless @IF NOT EXISTS@ is used in which case the operation is a no-op.
|
||||
* @DROP@: Removes a column from the table. Dropped columns will immediately become unavailable in the queries and will not be included in compacted sstables in the future. If a column is readded, queries won't return values written before the column was last dropped. It is assumed that timestamps represent actual time, so if this is not your case, you should NOT readd previously dropped columns. Columns can't be dropped from tables defined with the @COMPACT STORAGE@ option. If the dropped column does not already exist, the statement will return an error, unless @IF EXISTS@ is used in which case the operation is a no-op.
|
||||
* @ALTER@: Alters an existing column. It can be used to set its data mask, or to set it as unmasked. The data mask is any function meant to obscure the real values of the column.
|
||||
* @RENAME@ a primary key column of a table. Non primary key columns cannot be renamed. Furthermore, renaming a column to another name which already exists isn't allowed. It's important to keep in mind that renamed columns shouldn't have dependent secondary indexes. If the renamed column does not already exist, the statement will return an error, unless @IF EXISTS@ is used in which case the operation is a no-op.
|
||||
* @DROP COMPACT STORAGE@: Removes Thrift compatibility mode from the table.
|
||||
* @WITH@: Allows to update the options of the table. The "supported @<option>@":#createTableOptions (and syntax) are the same as for the @CREATE TABLE@ statement except that @COMPACT STORAGE@ is not supported. Note that setting any @compaction@ sub-options has the effect of erasing all previous @compaction@ options, so you need to re-specify all the sub-options if you want to keep them. The same note applies to the set of @compression@ sub-options.
|
||||
|
|
@ -1518,6 +1525,8 @@ The full set of available permissions is:
|
|||
* @AUTHORIZE@
|
||||
* @DESCRIBE@
|
||||
* @EXECUTE@
|
||||
* @UNMASK@
|
||||
* @SELECT_MASKED@
|
||||
|
||||
Not all permissions are applicable to every type of resource. For instance, @EXECUTE@ is only relevant in the context of functions or mbeans; granting @EXECUTE@ on a resource representing a table is nonsensical. Attempting to @GRANT@ a permission on resource to which it cannot be applied results in an error response. The following illustrates which permissions can be granted on which types of resource, and which statements are enabled by that permission.
|
||||
|
||||
|
|
@ -1577,6 +1586,12 @@ Not all permissions are applicable to every type of resource. For instance, @EXE
|
|||
| @EXECUTE@ | @ALL MBEANS@ |Execute operations on any mbean|
|
||||
| @EXECUTE@ | @MBEANS@ |Execute operations on any mbean matching a wildcard pattern|
|
||||
| @EXECUTE@ | @MBEAN@ |Execute operations on named mbean|
|
||||
| @UNMASK@ | @ALL KEYSPACES@ |See the clear contents of masked columns on any table|
|
||||
| @UNMASK@ | @KEYSPACE@ |See the clear contents of masked columns on any table in keyspace|
|
||||
| @UNMASK@ | @TABLE@ |See the clear contents of masked columns on the specified table|
|
||||
| @SELECT_MASKED@ | @ALL KEYSPACES@ |Select restricting masked columns on any table|
|
||||
| @SELECT_MASKED@ | @KEYSPACE@ |Select restricting masked columns on any table in keyspace|
|
||||
| @SELECT_MASKED@ | @TABLE@ |Select restricting masked columns on the specified table|
|
||||
|
||||
|
||||
h3(#grantPermissionsStmt). GRANT PERMISSION
|
||||
|
|
@ -1586,7 +1601,7 @@ __Syntax:__
|
|||
bc(syntax)..
|
||||
<grant-permission-stmt> ::= GRANT ( ALL ( PERMISSIONS )? | <permission> ( PERMISSION )? (, PERMISSION)* ) ON <resource> TO <identifier>
|
||||
|
||||
<permission> ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE | EXECUTE
|
||||
<permission> ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE | EXECUTE | UNMASK | SELECT_MASKED
|
||||
|
||||
<resource> ::= ALL KEYSPACES
|
||||
| KEYSPACE <identifier>
|
||||
|
|
@ -1643,7 +1658,7 @@ __Syntax:__
|
|||
bc(syntax)..
|
||||
<revoke-permission-stmt> ::= REVOKE ( ALL ( PERMISSIONS )? | <permission> ( PERMISSION )? (, PERMISSION)* ) ON <resource> FROM <identifier>
|
||||
|
||||
<permission> ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE | EXECUTE
|
||||
<permission> ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE | EXECUTE | UNMASK | SELECT_MASKED
|
||||
|
||||
<resource> ::= ALL KEYSPACES
|
||||
| KEYSPACE <identifier>
|
||||
|
|
@ -2484,6 +2499,7 @@ CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywo
|
|||
| @LIST@ | no |
|
||||
| @LOGIN@ | no |
|
||||
| @MAP@ | no |
|
||||
| @MASKED@ | no |
|
||||
| @MATERIALIZED@ | yes |
|
||||
| @MBEAN@ | yes |
|
||||
| @MBEANS@ | yes |
|
||||
|
|
@ -2513,6 +2529,7 @@ CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywo
|
|||
| @ROLES@ | no |
|
||||
| @SCHEMA@ | yes |
|
||||
| @SELECT@ | yes |
|
||||
| @SELECT_MASKED@| no |
|
||||
| @SET@ | yes |
|
||||
| @SFUNC@ | no |
|
||||
| @SMALLINT@ | no |
|
||||
|
|
@ -2534,6 +2551,7 @@ CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywo
|
|||
| @TUPLE@ | no |
|
||||
| @TYPE@ | no |
|
||||
| @UNLOGGED@ | yes |
|
||||
| @UNMASK@ | no |
|
||||
| @UNSET@ | yes |
|
||||
| @UPDATE@ | yes |
|
||||
| @USE@ | yes |
|
||||
|
|
@ -2548,7 +2566,7 @@ CQL distinguishes between _reserved_ and _non-reserved_ keywords. Reserved keywo
|
|||
| @WHERE@ | yes |
|
||||
| @WITH@ | yes |
|
||||
| @WRITETIME@ | no |
|
||||
| @MAXWRITETIME@ | no |
|
||||
| @MAXWRITETIME@ | no |
|
||||
|
||||
h2(#appendixB). Appendix B: CQL Reserved Types
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
alter_table_statement::= ALTER TABLE [ IF EXISTS ] table_name alter_table_instruction
|
||||
alter_table_instruction::= ADD [ IF NOT EXISTS ] column_name cql_type ( ',' column_name cql_type )*
|
||||
alter_table_instruction::= ADD [ IF NOT EXISTS ] column_definition ( ',' column_definition)*
|
||||
| DROP [ IF EXISTS ] column_name ( ',' column_name )*
|
||||
| RENAME [ IF EXISTS ] column_name to column_name (AND column_name to column_name)*
|
||||
| ALTER [ IF EXISTS ] column_name ( column_mask | DROP MASKED )
|
||||
| WITH options
|
||||
column_definition::= column_name cql_type [ column_mask]
|
||||
column_mask::= MASKED WITH ( DEFAULT | function_name '(' term ( ',' term )* ')' )
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ create_table_statement::= CREATE TABLE [ IF NOT EXISTS ] table_name '('
|
|||
column_definition ( ',' column_definition )*
|
||||
[ ',' PRIMARY KEY '(' primary_key ')' ]
|
||||
')' [ WITH table_options ]
|
||||
column_definition::= column_name cql_type [ STATIC ] [ PRIMARY KEY]
|
||||
column_definition::= column_name cql_type [ STATIC ] [ column_mask ] [ PRIMARY KEY]
|
||||
column_mask::= MASKED WITH ( DEFAULT | function_name '(' term ( ',' term )* ')' )
|
||||
primary_key::= partition_key [ ',' clustering_columns ]
|
||||
partition_key::= column_name | '(' column_name ( ',' column_name )* ')'
|
||||
clustering_columns::= column_name ( ',' column_name )*
|
||||
table_options:=: COMPACT STORAGE [ AND table_options ]
|
||||
table_options::= COMPACT STORAGE [ AND table_options ]
|
||||
| CLUSTERING ORDER BY '(' clustering_order ')'
|
||||
[ AND table_options ] | options
|
||||
clustering_order::= column_name (ASC | DESC) ( ',' column_name (ASC | DESC) )*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
grant_permission_statement ::= GRANT permissions ON resource TO role_name
|
||||
permissions ::= ALL [ PERMISSIONS ] | permission [ PERMISSION ]
|
||||
permission ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESCRIBE | EXECUTE
|
||||
permission ::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESCRIBE | EXECUTE | UNMASK | SELECT_MASKED
|
||||
resource ::= ALL KEYSPACES
|
||||
| KEYSPACE keyspace_name
|
||||
| [ TABLE ] table_name
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE patients ALTER name MASKED WITH mask_default();
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE patients (
|
||||
id timeuuid PRIMARY KEY,
|
||||
name text MASKED WITH mask_inner(1, null),
|
||||
birth date MASKED WITH mask_default()
|
||||
);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
CREATE FUNCTION redact(input text)
|
||||
CALLED ON NULL INPUT
|
||||
RETURNS text
|
||||
LANGUAGE java
|
||||
AS 'return "redacted";
|
||||
|
||||
CREATE TABLE patients (
|
||||
id timeuuid PRIMARY KEY,
|
||||
name text MASKED WITH redact(),
|
||||
birth date
|
||||
);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
CREATE USER privileged WITH PASSWORD 'xyz';
|
||||
GRANT SELECT ON TABLE patients TO privileged;
|
||||
GRANT UNMASK ON TABLE patients TO privileged;
|
||||
|
||||
CREATE USER unprivileged WITH PASSWORD 'xyz';
|
||||
GRANT SELECT ON TABLE patients TO unprivileged;
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE patients ALTER name DROP MASKED;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
INSERT INTO patients(id, name, birth) VALUES (now(), 'alice', '1984-01-02');
|
||||
INSERT INTO patients(id, name, birth) VALUES (now(), 'bob', '1982-02-03');
|
||||
|
|
@ -0,0 +1 @@
|
|||
REVOKE UNMASK ON TABLE patients FROM privileged;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
SELECT name, birth FROM patients;
|
||||
|
||||
// name | birth
|
||||
// -------+------------
|
||||
// a**** | 1970-01-01
|
||||
// b** | 1970-01-01
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
CREATE USER trusted_user WITH PASSWORD 'xyz';
|
||||
GRANT SELECT, SELECT_MASKED ON TABLE patients TO trusted_user;
|
||||
LOGIN trusted_user
|
||||
SELECT name, birth FROM patients WHERE name = 'Alice' ALLOW FILTERING;
|
||||
|
||||
// name | birth
|
||||
// -------+------------
|
||||
// a**** | 1970-01-01
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
LOGIN privileged
|
||||
SELECT name, birth FROM patients;
|
||||
|
||||
// name | birth
|
||||
// -------+------------
|
||||
// alice | 1984-01-02
|
||||
// bob | 1982-02-03
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
CREATE USER untrusted_user WITH PASSWORD 'xyz';
|
||||
GRANT SELECT ON TABLE patients TO untrusted_user;
|
||||
LOGIN untrusted_user
|
||||
SELECT name, birth FROM patients WHERE name = 'Alice' ALLOW FILTERING;
|
||||
|
||||
// Unauthorized: Error from server: code=2100 [Unauthorized] message="User untrusted_user has no UNMASK nor SELECT_UNMASK permission on table k.patients"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
LOGIN unprivileged
|
||||
SELECT name, birth FROM patients;
|
||||
|
||||
// name | birth
|
||||
// -------+------------
|
||||
// a**** | 1970-01-01
|
||||
// b** | 1970-01-01
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
CREATE TABLE patients (
|
||||
id timeuuid PRIMARY KEY,
|
||||
name text,
|
||||
birth date
|
||||
);
|
||||
|
||||
INSERT INTO patients(id, name, birth) VALUES (now(), 'alice', '1982-01-02');
|
||||
INSERT INTO patients(id, name, birth) VALUES (now(), 'bob', '1982-01-02');
|
||||
|
||||
SELECT mask_inner(name, 1, null), mask_default(birth) FROM patients;
|
||||
|
||||
// system.mask_inner(name, 1, NULL) | system.mask_default(birth)
|
||||
// -----------------------------------+----------------------------
|
||||
// b** | 1970-01-01
|
||||
// a**** | 1970-01-01
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
*** xref:cql/functions.adoc[Functions]
|
||||
*** xref:cql/json.adoc[JSON]
|
||||
*** xref:cql/security.adoc[Security]
|
||||
*** xref:cql/dynamic_data_masking.adoc[Dynamic data masking]
|
||||
*** xref:cql/triggers.adoc[Triggers]
|
||||
*** xref:cql/appendices.adoc[Appendices]
|
||||
*** xref:cql/changes.adoc[Changes]
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ or not.
|
|||
|`LIST` |no
|
||||
|`LOGIN` |no
|
||||
|`MAP` |no
|
||||
|`MASKED` |no
|
||||
|`MODIFY` |yes
|
||||
|`NAN` |yes
|
||||
|`NOLOGIN` |no
|
||||
|
|
@ -106,6 +107,7 @@ or not.
|
|||
|`ROLES` |no
|
||||
|`SCHEMA` |yes
|
||||
|`SELECT` |yes
|
||||
|`SELECT_MASKED` |no
|
||||
|`SET` |yes
|
||||
|`SFUNC` |no
|
||||
|`SMALLINT` |no
|
||||
|
|
@ -127,6 +129,7 @@ or not.
|
|||
|`TUPLE` |no
|
||||
|`TYPE` |no
|
||||
|`UNLOGGED` |yes
|
||||
|`UNMASK` |no
|
||||
|`UPDATE` |yes
|
||||
|`USE` |yes
|
||||
|`USER` |no
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ The following describes the changes in each version of CQL.
|
|||
== 3.4.7
|
||||
|
||||
* Remove deprecated functions `dateOf` and `unixTimestampOf`, replaced by `toTimestamp` and `toUnixTimestamp` (`18328`)
|
||||
* Added support for attaching masking functions to table columns (`18068`)
|
||||
* Add UNMASK permission (`18069`)
|
||||
* Add SELECT_MASKED permission (`18070`)
|
||||
* Add support for using UDFs as masking functions (`18071`)
|
||||
|
||||
== 3.4.6
|
||||
|
||||
|
|
|
|||
|
|
@ -2250,6 +2250,8 @@ The full set of available permissions is:
|
|||
* `AUTHORIZE`
|
||||
* `DESCRIBE`
|
||||
* `EXECUTE`
|
||||
* `UNMASK`
|
||||
* `SELECT_MASKED`
|
||||
|
||||
Not all permissions are applicable to every type of resource. For
|
||||
instance, `EXECUTE` is only relevant in the context of functions or
|
||||
|
|
@ -2408,6 +2410,18 @@ use of function in `CREATE AGGREGATE` | | |
|
|||
wildcard pattern | | |
|
||||
|
||||
|`EXECUTE` |`MBEAN` |Execute operations on named mbean | | |
|
||||
|
||||
|`UNMASK` |`ALL KEYSPACES` |See the clear contents of masked columns on any table | | |
|
||||
|
||||
|`UNMASK` |`KEYSPACE` |See the clear contents of masked columns on any table in keyspace | | |
|
||||
|
||||
|`UNMASK` |`TABLE` |See the clear contents of masked columns on the specified table | | |
|
||||
|
||||
|`SELECT_MASKED` | `ALL KEYSPACES` | `SELECT` restricting masked columns on any table | | |
|
||||
|
||||
|`SELECT_MASKED` | `KEYSPACE` | `SELECT` restricting masked columns on any table in specified keyspace | | |
|
||||
|
||||
|`SELECT_MASKED` | `TABLE` | `SELECT` restricting masked columns on the specified table | | |
|
||||
|===
|
||||
|
||||
[[grantPermissionsStmt]]
|
||||
|
|
@ -2418,7 +2432,7 @@ _Syntax:_
|
|||
bc(syntax).. +
|
||||
::= GRANT ( ALL ( PERMISSIONS )? | ( PERMISSION )? ) ON TO
|
||||
|
||||
::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE |
|
||||
::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE | UNMASK | SELECT_MASKED
|
||||
EXECUTE
|
||||
|
||||
::= ALL KEYSPACES +
|
||||
|
|
@ -2490,7 +2504,7 @@ _Syntax:_
|
|||
bc(syntax).. +
|
||||
::= REVOKE ( ALL ( PERMISSIONS )? | ( PERMISSION )? ) ON FROM
|
||||
|
||||
::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE |
|
||||
::= CREATE | ALTER | DROP | SELECT | MODIFY | AUTHORIZE | DESRIBE | UNMASK | SELECT_MASKED
|
||||
EXECUTE
|
||||
|
||||
::= ALL KEYSPACES +
|
||||
|
|
@ -3594,6 +3608,7 @@ or not.
|
|||
|`LIST` |no
|
||||
|`LOGIN` |no
|
||||
|`MAP` |no
|
||||
|`MASKED` |no
|
||||
|`MATERIALIZED` |yes
|
||||
|`MBEAN` |yes
|
||||
|`MBEANS` |yes
|
||||
|
|
@ -3623,6 +3638,7 @@ or not.
|
|||
|`ROLES` |no
|
||||
|`SCHEMA` |yes
|
||||
|`SELECT` |yes
|
||||
|`SELECT_MASKED` |no
|
||||
|`SET` |yes
|
||||
|`SFUNC` |no
|
||||
|`SMALLINT` |no
|
||||
|
|
@ -3644,6 +3660,7 @@ or not.
|
|||
|`TUPLE` |no
|
||||
|`TYPE` |no
|
||||
|`UNLOGGED` |yes
|
||||
|`UNMASK` |no
|
||||
|`UNSET` |yes
|
||||
|`UPDATE` |yes
|
||||
|`USE` |yes
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
= Dynamic Data Masking
|
||||
|
||||
Dynamic data masking (DDM) allows to obscure sensitive information while still allowing access to the masked columns.
|
||||
DDM doesn't change the stored data. Instead, it just presents the data on their obscured form during `SELECT` queries.
|
||||
This aims to provide some degree of protection against accidental data exposure. However, it's important to know that
|
||||
anyone with direct access to the sstable files will be able to read the clear data.
|
||||
|
||||
== Masking functions
|
||||
|
||||
DDM is based on a set of CQL native functions that obscure sensitive information. The available functions are:
|
||||
|
||||
include::partial$masking_functions.adoc[]
|
||||
|
||||
Those functions can be discretionarily used on `SELECT` queries to get an obscured view of the data. For example:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/select_with_mask_functions.cql[]
|
||||
----
|
||||
|
||||
== Attaching masking functions to table columns
|
||||
|
||||
The masking functions can be permanently attached to the columns of a table.
|
||||
In that case, `SELECT` queries will always return the column values in their masked form.
|
||||
The masking will be transparent for the users running `SELECT` queries,
|
||||
so their only way to know that a column is masked will be consulting the table definition.
|
||||
|
||||
This is an optional feature that should be enabled with the `dynamic_data_masking_enabled` property in `cassandra.yaml`,
|
||||
since it's disabled by default.
|
||||
|
||||
The masks of the columns of a table can be defined on `CREATE TABLE` queries:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_create_table.cql[]
|
||||
----
|
||||
|
||||
Note that in the example above we are referencing the `mask_inner` function with two arguments.
|
||||
However, that CQL function actually has three arguments when explicitely used on `SELECT` queries.
|
||||
The first argument is always ommitted when attaching the function to a schema column.
|
||||
The value of that first argument is always interpreted as the value of the masked column, in this case a `text` column.
|
||||
For the same reason the call to `mask_default` attached to the column doesn't have any argument,
|
||||
even when that function requires one argument when explicitely used on `SELECT` queries.
|
||||
|
||||
Data can be inserted into the masked table as usual. For example:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_insert_data.cql[]
|
||||
----
|
||||
|
||||
The attached column masks will make `SELECT` queries automatically return masked data,
|
||||
without the need of including the masking function on the query:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_select_with_masked_columns.cql[]
|
||||
----
|
||||
|
||||
The masking function attached to a column can be changed with an `ALTER TABLE` query:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_alter_mask.cql[]
|
||||
----
|
||||
|
||||
In a similar way, a masking function can be dettached from a column with an `ALTER TABLE` query:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_drop_mask.cql[]
|
||||
----
|
||||
|
||||
== Permissions
|
||||
|
||||
The `UNMASK` permission allows users to retrieve the unmasked values of masked columns.
|
||||
The masks will only be applied to the results of a `SELECT` query if the user doesn't have the `UNMASK` permission.
|
||||
Ordinary users are created without the `UNMASK` permission, whereas superusers do have it.
|
||||
|
||||
As an example, suppose that we have a table with masked columns:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_create_table.cql[]
|
||||
----
|
||||
|
||||
And we insert some data into the table:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_insert_data.cql[]
|
||||
----
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_select_without_unmask_permission.cql[]
|
||||
----
|
||||
|
||||
Then we create two users with `SELECT` permission for the table, but we only grant the `UNMASK` permission to one of
|
||||
the users:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_create_users.cql[]
|
||||
----
|
||||
|
||||
We can now see that the user with the `UNMASK` permission can see the clear data, without any masking:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_select_with_unmask_permission.cql[]
|
||||
----
|
||||
|
||||
However, the user without the `UNMASK` permission can only see the masked data:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_select_without_unmask_permission.cql[]
|
||||
----
|
||||
|
||||
The `UNMASK` permission works as any other permission. Thus, it can be revoked in any moment:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_revoke_unmask.cql[]
|
||||
----
|
||||
|
||||
Please note that the anonymous user that is used when authentication is disabled has all the permissions.
|
||||
Since it includes the `UNMASK` permission, that anonymous user will always see the clear data.
|
||||
In other words, attaching data masking functions to columns only makes sense if authentication is enabled.
|
||||
|
||||
Users without the `UNMASK` permission are not allowed to use masked columns in the `WHERE` clause of a `SELECT` query.
|
||||
This prevents malicious users from figuring out the clear data by running exhaustive queries. For instance:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_select_without_select_masked.cql[]
|
||||
----
|
||||
|
||||
However, there are some use cases where trusted database users just need a useful way to produce masked data
|
||||
that will be served to untrusted external users.
|
||||
For example, a trusted app can connect to the database and extract masked data that will be served to its end users.
|
||||
In that case the trusted user (the app) can be given the `SELECT_MASKED` permission.
|
||||
That permission allows to use masked columns in the `WHERE` clause of a `SELECT` query,
|
||||
while still seeing the masked data in the query results. For instance:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_select_with_select_masked.cql[]
|
||||
----
|
||||
|
||||
== Custom functions
|
||||
|
||||
xref:cql/functions.adoc#user-defined-scalar-functions[User-defined functions (UDFs)] can be attached to a table column.
|
||||
The UDFs used for masking should belong to the same keyspace as the masked table.
|
||||
The column value to mask will be passed as the first argument of the attached UDF.
|
||||
Thus, the UDFs attached to a column should have at least one argument,
|
||||
and that argument should have the same type as the masked column.
|
||||
Also, the attached UDF should return values of the same type as the maked column. For instance:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_create_table_with_udf.cql[]
|
||||
----
|
||||
|
||||
This creates a dependency between the table schema and the functions.
|
||||
Any attempt to drop the function will be rejected while this dependency exists.
|
||||
Thus, to drop the function you should first drop the mask.
|
||||
This can be done with:
|
||||
|
||||
[source,cql]
|
||||
----
|
||||
include::example$CQL/ddm_drop_mask.cql[]
|
||||
----
|
||||
|
||||
Dropping the column, or its containing table, or its containing keyspace would also remove the dependency.
|
||||
|
||||
xref:cql/functions.adoc#aggregate-functions[Aggregate functions] cannot be used as masking functions.
|
||||
|
|
@ -277,71 +277,12 @@ A number of functions are provided to operate on collection columns.
|
|||
| `collection_avg` | numeric `set` or `list` | Computes the average of the elements in the collection argument. The average of an empty collection returns zero. The returned value is of the same type as the input collection elements, which might include rounding and truncations. For example `collection_avg([1, 2])` returns `1` instead of `1.5`.
|
||||
|===
|
||||
|
||||
[[data-masking-functions]]
|
||||
===== Data masking functions
|
||||
|
||||
A number of functions allow to obscure the real contents of a column containing sensitive data.
|
||||
|
||||
[cols=",",options="header",]
|
||||
|===
|
||||
|Function | Description
|
||||
|
||||
| `mask_null(value)` | Replaces the first argument by a `null` column. The returned value is always an absent column, as it didn't exist, and not a not-null column representing a `null` value.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_null('Alice')` -> `null`
|
||||
|
||||
`mask_null(123)` -> `null`
|
||||
|
||||
| `mask_default(value)` | Replaces its argument by an arbitrary, fixed default value of the same type. This will be `\***\***` for text values, zero for numeric values, `false` for booleans, etc.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_default('Alice')` -> `'\****'`
|
||||
|
||||
`mask_default(123)` -> `0`
|
||||
|
||||
| `mask_replace(value, replacement])` | Replaces the first argument by the replacement value on the second argument. The replacement value needs to have the same type as the replaced value.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_replace('Alice', 'REDACTED')` -> `'REDACTED'`
|
||||
|
||||
`mask_replace(123, -1)` -> `-1`
|
||||
|
||||
| `mask_inner(value, begin, end, [padding])` | Returns a copy of the first `text`, `varchar` or `ascii` argument, replacing each character except the first and last ones by a padding character. The 2nd and 3rd arguments are the size of the exposed prefix and suffix. The optional 4th argument is the padding character, `\*` by default.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_inner('Alice', 1, 2)` -> `'A**ce'`
|
||||
|
||||
`mask_inner('Alice', 1, null)` -> `'A****'`
|
||||
|
||||
`mask_inner('Alice', null, 2)` -> `'***ce'`
|
||||
|
||||
`mask_inner('Alice', 2, 1, '\#')` -> `'Al##e'`
|
||||
|
||||
| `mask_outer(value, begin, end, [padding])` | Returns a copy of the first `text`, `varchar` or `ascii` argument, replacing the first and last character by a padding character. The 2nd and 3rd arguments are the size of the exposed prefix and suffix. The optional 4th argument is the padding character, `\*` by default.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_outer('Alice', 1, 2)` -> `'*li**'`
|
||||
|
||||
`mask_outer('Alice', 1, null)` -> `'*lice'`
|
||||
|
||||
`mask_outer('Alice', null, 2)` -> `'Ali**'`
|
||||
|
||||
`mask_outer('Alice', 2, 1, '\#')` -> `'##ic#'`
|
||||
|
||||
| `mask_hash(value, [algorithm])` | Returns a `blob` containing the hash of the first argument. The optional 2nd argument is the hashing algorithm to be used, according the available Java security provider. The default hashing algorithm is `SHA-256`.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_hash('Alice')`
|
||||
|
||||
`mask_hash('Alice', 'SHA-512')`
|
||||
|
||||
|===
|
||||
include::partial$masking_functions.adoc[]
|
||||
|
||||
[[user-defined-scalar-functions]]
|
||||
==== User-defined functions
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ For that reason, when used in this document, these terms (tables, rows and colum
|
|||
* xref:cql/functions.adoc[Functions]
|
||||
* xref:cql/json.adoc[JSON]
|
||||
* xref:cql/security.adoc[CQL security]
|
||||
* xref:cql/dynamic_data_masking.adoc[Dynamic data masking]
|
||||
* xref:cql/triggers.adoc[Triggers]
|
||||
* xref:cql/appendices.adoc[Appendices]
|
||||
* xref:cql/changes.adoc[Changes]
|
||||
|
|
|
|||
|
|
@ -352,6 +352,8 @@ The full set of available permissions is:
|
|||
* `AUTHORIZE`
|
||||
* `DESCRIBE`
|
||||
* `EXECUTE`
|
||||
* `UNMASK`
|
||||
* `SELECT_MASKED`
|
||||
|
||||
Not all permissions are applicable to every type of resource. For
|
||||
instance, `EXECUTE` is only relevant in the context of functions or
|
||||
|
|
@ -477,6 +479,18 @@ and use of any function in keyspace in `CREATE AGGREGATE`
|
|||
| `EXECUTE` | `MBEANS` | Execute operations on any mbean matching a wildcard pattern
|
||||
|
||||
| `EXECUTE` | `MBEAN` | Execute operations on named mbean
|
||||
|
||||
|`UNMASK` |`ALL KEYSPACES` | See the clear contents of masked columns on any table
|
||||
|
||||
|`UNMASK` |`KEYSPACE` | See the clear contents of masked columns on any table in keyspace
|
||||
|
||||
|`UNMASK` |`TABLE` | See the clear contents of masked columns on the specified table
|
||||
|
||||
|`SELECT_MASKED` | `ALL KEYSPACES` | `SELECT` restricting masked columns on any table
|
||||
|
||||
|`SELECT_MASKED` | `KEYSPACE` | `SELECT` restricting masked columns on any table in specified keyspace
|
||||
|
||||
|`SELECT_MASKED` | `TABLE` | `SELECT` restricting masked columns on the specified table
|
||||
|===
|
||||
|
||||
[[grant-permission-statement]]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
[cols=",",options="header",]
|
||||
|===
|
||||
|Function | Description
|
||||
|
||||
| `mask_null(value)` | Replaces the first argument by a `null` column. The returned value is always an absent column, as it didn't exist, and not a not-null column representing a `null` value.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_null('Alice')` -> `null`
|
||||
|
||||
`mask_null(123)` -> `null`
|
||||
|
||||
| `mask_default(value)` | Replaces its argument by an arbitrary, fixed default value of the same type. This will be `\***\***` for text values, zero for numeric values, `false` for booleans, etc.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_default('Alice')` -> `'\****'`
|
||||
|
||||
`mask_default(123)` -> `0`
|
||||
|
||||
| `mask_replace(value, replacement])` | Replaces the first argument by the replacement value on the second argument. The replacement value needs to have the same type as the replaced value.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_replace('Alice', 'REDACTED')` -> `'REDACTED'`
|
||||
|
||||
`mask_replace(123, -1)` -> `-1`
|
||||
|
||||
| `mask_inner(value, begin, end, [padding])` | Returns a copy of the first `text`, `varchar` or `ascii` argument, replacing each character except the first and last ones by a padding character. The 2nd and 3rd arguments are the size of the exposed prefix and suffix. The optional 4th argument is the padding character, `\*` by default.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_inner('Alice', 1, 2)` -> `'A**ce'`
|
||||
|
||||
`mask_inner('Alice', 1, null)` -> `'A****'`
|
||||
|
||||
`mask_inner('Alice', null, 2)` -> `'***ce'`
|
||||
|
||||
`mask_inner('Alice', 2, 1, '\#')` -> `'Al##e'`
|
||||
|
||||
| `mask_outer(value, begin, end, [padding])` | Returns a copy of the first `text`, `varchar` or `ascii` argument, replacing the first and last character by a padding character. The 2nd and 3rd arguments are the size of the exposed prefix and suffix. The optional 4th argument is the padding character, `\*` by default.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_outer('Alice', 1, 2)` -> `'*li**'`
|
||||
|
||||
`mask_outer('Alice', 1, null)` -> `'*lice'`
|
||||
|
||||
`mask_outer('Alice', null, 2)` -> `'Ali**'`
|
||||
|
||||
`mask_outer('Alice', 2, 1, '\#')` -> `'##ic#'`
|
||||
|
||||
| `mask_hash(value, [algorithm])` | Returns a `blob` containing the hash of the first argument. The optional 2nd argument is the hashing algorithm to be used, according the available Java security provider. The default hashing algorithm is `SHA-256`.
|
||||
|
||||
Examples:
|
||||
|
||||
`mask_hash('Alice')`
|
||||
|
||||
`mask_hash('Alice', 'SHA-512')`
|
||||
|
||||
|===
|
||||
|
|
@ -305,7 +305,9 @@ JUNK ::= /([ \t\r\f\v]+|(--|[/][/])[^\n\r]*([\n\r]|$)|[/][*].*?[*][/])/ ;
|
|||
|
||||
<userType> ::= utname=<cfOrKsName> ;
|
||||
|
||||
<storageType> ::= <simpleStorageType> | <collectionType> | <frozenCollectionType> | <userType> ;
|
||||
<storageType> ::= ( <simpleStorageType> | <collectionType> | <frozenCollectionType> | <userType> ) ( <column_mask> )? ;
|
||||
|
||||
<column_mask> ::= "MASKED" "WITH" ( "DEFAULT" | <functionName> <selectionFunctionArguments> );
|
||||
|
||||
# Note: autocomplete for frozen collection types does not handle nesting past depth 1 properly,
|
||||
# but that's a lot of work to fix for little benefit.
|
||||
|
|
@ -1418,6 +1420,7 @@ syntax_rules += r'''
|
|||
| "WITH" <cfamProperty> ( "AND" <cfamProperty> )*
|
||||
| "RENAME" ("IF" "EXISTS")? existcol=<cident> "TO" newcol=<cident>
|
||||
( "AND" existcol=<cident> "TO" newcol=<cident> )*
|
||||
| "ALTER" ("IF" "EXISTS")? existcol=<cident> ( <column_mask> | "DROP" "MASKED" )
|
||||
;
|
||||
|
||||
<alterUserTypeStatement> ::= "ALTER" "TYPE" ("IF" "EXISTS")? ut=<userTypeName>
|
||||
|
|
@ -1530,6 +1533,8 @@ syntax_rules += r'''
|
|||
| "MODIFY"
|
||||
| "DESCRIBE"
|
||||
| "EXECUTE"
|
||||
| "UNMASK"
|
||||
| "SELECT_MASKED"
|
||||
;
|
||||
|
||||
<permissionExpr> ::= ( [newpermission]=<permission> "PERMISSION"? ( "," [newpermission]=<permission> "PERMISSION"? )* )
|
||||
|
|
|
|||
|
|
@ -620,7 +620,12 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
self.trycompletions(prefix + ' new_table (col_a ine',
|
||||
immediate='t ')
|
||||
self.trycompletions(prefix + ' new_table (col_a int ',
|
||||
choices=[',', 'PRIMARY'])
|
||||
choices=[',', 'MASKED', 'PRIMARY'])
|
||||
self.trycompletions(prefix + ' new_table (col_a int M',
|
||||
immediate='ASKED WITH ')
|
||||
self.trycompletions(prefix + ' new_table (col_a int MASKED WITH ',
|
||||
choices=['DEFAULT', self.cqlsh.keyspace + '.', 'system.'],
|
||||
other_choices_ok=True)
|
||||
self.trycompletions(prefix + ' new_table (col_a int P',
|
||||
immediate='RIMARY KEY ')
|
||||
self.trycompletions(prefix + ' new_table (col_a int PRIMARY KEY ',
|
||||
|
|
@ -876,7 +881,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
self.trycompletions("GR",
|
||||
immediate='ANT ')
|
||||
self.trycompletions("GRANT ",
|
||||
choices=['ALL', 'ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'MODIFY', 'SELECT'],
|
||||
choices=['ALL', 'ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'MODIFY', 'SELECT', 'UNMASK', 'SELECT_MASKED'],
|
||||
other_choices_ok=True)
|
||||
self.trycompletions("GRANT MODIFY ",
|
||||
choices=[',', 'ON', 'PERMISSION'])
|
||||
|
|
@ -885,7 +890,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
self.trycompletions("GRANT MODIFY PERMISSION ",
|
||||
choices=[',', 'ON'])
|
||||
self.trycompletions("GRANT MODIFY PERMISSION, ",
|
||||
choices=['ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'SELECT'])
|
||||
choices=['ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'SELECT', 'UNMASK', 'SELECT_MASKED'])
|
||||
self.trycompletions("GRANT MODIFY PERMISSION, D",
|
||||
choices=['DESCRIBE', 'DROP'])
|
||||
self.trycompletions("GRANT MODIFY PERMISSION, DR",
|
||||
|
|
@ -907,7 +912,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
self.trycompletions("RE",
|
||||
immediate='VOKE ')
|
||||
self.trycompletions("REVOKE ",
|
||||
choices=['ALL', 'ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'MODIFY', 'SELECT'],
|
||||
choices=['ALL', 'ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'MODIFY', 'SELECT', 'UNMASK', 'SELECT_MASKED'],
|
||||
other_choices_ok=True)
|
||||
self.trycompletions("REVOKE MODIFY ",
|
||||
choices=[',', 'ON', 'PERMISSION'])
|
||||
|
|
@ -916,7 +921,7 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
self.trycompletions("REVOKE MODIFY PERMISSION ",
|
||||
choices=[',', 'ON'])
|
||||
self.trycompletions("REVOKE MODIFY PERMISSION, ",
|
||||
choices=['ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'SELECT'])
|
||||
choices=['ALTER', 'AUTHORIZE', 'CREATE', 'DESCRIBE', 'DROP', 'EXECUTE', 'SELECT', 'UNMASK', 'SELECT_MASKED'])
|
||||
self.trycompletions("REVOKE MODIFY PERMISSION, D",
|
||||
choices=['DESCRIBE', 'DROP'])
|
||||
self.trycompletions("REVOKE MODIFY PERMISSION, DR",
|
||||
|
|
@ -954,9 +959,23 @@ class TestCqlshCompletion(CqlshCompletionCase):
|
|||
self.trycompletions('ALTER TABLE IF EXISTS new_table ADD ', choices=['<new_column_name>', 'IF'])
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ADD IF NOT EXISTS ', choices=['<new_column_name>'])
|
||||
self.trycompletions('ALTER TABLE new_table ADD IF NOT EXISTS ', choices=['<new_column_name>'])
|
||||
self.trycompletions('ALTER TABLE new_table ADD col int ', choices=[';', 'MASKED', 'static'])
|
||||
self.trycompletions('ALTER TABLE new_table ADD col int M', immediate='ASKED WITH ')
|
||||
self.trycompletions('ALTER TABLE new_table ADD col int MASKED WITH ',
|
||||
choices=['DEFAULT', self.cqlsh.keyspace + '.', 'system.'],
|
||||
other_choices_ok=True)
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table RENAME ', choices=['IF', '<quotedName>', '<identifier>'])
|
||||
self.trycompletions('ALTER TABLE new_table RENAME ', choices=['IF', '<quotedName>', '<identifier>'])
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table DROP ', choices=['IF', '<quotedName>', '<identifier>'])
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER ', choices=['IF', '<quotedName>', '<identifier>'])
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF E', immediate='XISTS ')
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col ', choices=['MASKED', 'DROP'])
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col M', immediate='ASKED WITH ')
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col MASKED WITH ',
|
||||
choices=['DEFAULT', self.cqlsh.keyspace + '.', 'system.'],
|
||||
other_choices_ok=True)
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col D', immediate='ROP MASKED ;')
|
||||
self.trycompletions('ALTER TABLE IF EXISTS new_table ALTER IF EXISTS col DROP M', immediate='ASKED ;')
|
||||
|
||||
def test_complete_in_alter_type(self):
|
||||
self.trycompletions('ALTER TYPE I', immediate='F EXISTS ')
|
||||
|
|
@ -991,7 +1010,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'])
|
||||
self.trycompletions('LIST ', choices=['ALL', 'AUTHORIZE', 'DESCRIBE', 'EXECUTE', 'ROLES', 'USERS', 'ALTER', 'CREATE', 'DROP', 'MODIFY', 'SELECT', 'UNMASK', 'SELECT_MASKED'])
|
||||
|
||||
|
||||
# Non-CQL Shell Commands
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import Parser,Lexer;
|
|||
import org.apache.cassandra.auth.*;
|
||||
import org.apache.cassandra.cql3.conditions.*;
|
||||
import org.apache.cassandra.cql3.functions.*;
|
||||
import org.apache.cassandra.cql3.functions.masking.*;
|
||||
import org.apache.cassandra.cql3.restrictions.CustomIndexExpression;
|
||||
import org.apache.cassandra.cql3.selection.*;
|
||||
import org.apache.cassandra.cql3.statements.*;
|
||||
|
|
|
|||
|
|
@ -218,6 +218,10 @@ K_DEFAULT: D E F A U L T;
|
|||
K_UNSET: U N S E T;
|
||||
K_LIKE: L I K E;
|
||||
|
||||
K_MASKED: M A S K E D;
|
||||
K_UNMASK: U N M A S K;
|
||||
K_SELECT_MASKED: S E L E C T '_' M A S K E D;
|
||||
|
||||
// Case-insensitive alpha characters
|
||||
fragment A: ('a'|'A');
|
||||
fragment B: ('b'|'B');
|
||||
|
|
|
|||
|
|
@ -782,11 +782,21 @@ tableDefinition[CreateTableStatement.Raw stmt]
|
|||
|
||||
tableColumns[CreateTableStatement.Raw stmt]
|
||||
@init { boolean isStatic = false; }
|
||||
: k=ident v=comparatorType (K_STATIC { isStatic = true; })? { $stmt.addColumn(k, v, isStatic); }
|
||||
: k=ident v=comparatorType (K_STATIC { isStatic = true; })? (mask=columnMask)? { $stmt.addColumn(k, v, isStatic, mask); }
|
||||
(K_PRIMARY K_KEY { $stmt.setPartitionKeyColumn(k); })?
|
||||
| K_PRIMARY K_KEY '(' tablePartitionKey[stmt] (',' c=ident { $stmt.markClusteringColumn(c); } )* ')'
|
||||
;
|
||||
|
||||
columnMask returns [ColumnMask.Raw mask]
|
||||
@init { List<Term.Raw> arguments = new ArrayList<>(); }
|
||||
: K_MASKED K_WITH name=functionName columnMaskArguments[arguments] { $mask = new ColumnMask.Raw(name, arguments); }
|
||||
| K_MASKED K_WITH K_DEFAULT { $mask = new ColumnMask.Raw(FunctionName.nativeFunction("mask_default"), arguments); }
|
||||
;
|
||||
|
||||
columnMaskArguments[List<Term.Raw> arguments]
|
||||
: '(' ')' | '(' c=term { arguments.add(c); } (',' cn=term { arguments.add(cn); })* ')'
|
||||
;
|
||||
|
||||
tablePartitionKey[CreateTableStatement.Raw stmt]
|
||||
@init {List<ColumnIdentifier> l = new ArrayList<ColumnIdentifier>();}
|
||||
@after{ $stmt.setPartitionKeyColumns(l); }
|
||||
|
|
@ -929,7 +939,9 @@ alterKeyspaceStatement returns [AlterKeyspaceStatement.Raw stmt]
|
|||
|
||||
/**
|
||||
* ALTER TABLE <table> ALTER <column> TYPE <newtype>;
|
||||
* ALTER TABLE [IF EXISTS] <table> ADD [IF NOT EXISTS] <column> <newtype>; | ALTER TABLE [IF EXISTS] <table> ADD [IF NOT EXISTS] (<column> <newtype>,<column1> <newtype1>..... <column n> <newtype n>)
|
||||
* ALTER TABLE [IF EXISTS] <table> ALTER [IF EXISTS] <column> MASKED WITH <maskFunction>);
|
||||
* ALTER TABLE [IF EXISTS] <table> ALTER [IF EXISTS] <column> DROP MASKED;
|
||||
* ALTER TABLE [IF EXISTS] <table> ADD [IF NOT EXISTS] <column> <newtype> <maskFunction>; | ALTER TABLE [IF EXISTS] <table> ADD [IF NOT EXISTS] (<column> <newtype> <maskFunction>, <column1> <newtype1> <maskFunction1>..... <column n> <newtype n> <maskFunction n>)
|
||||
* ALTER TABLE [IF EXISTS] <table> DROP [IF EXISTS] <column>; | ALTER TABLE [IF EXISTS] <table> DROP [IF EXISTS] ( <column>,<column1>.....<column n>)
|
||||
* ALTER TABLE [IF EXISTS] <table> RENAME [IF EXISTS] <column> TO <column>;
|
||||
* ALTER TABLE [IF EXISTS] <table> WITH <property> = <value>;
|
||||
|
|
@ -941,10 +953,14 @@ alterTableStatement returns [AlterTableStatement.Raw stmt]
|
|||
(
|
||||
K_ALTER id=cident K_TYPE v=comparatorType { $stmt.alter(id, v); }
|
||||
|
||||
| K_ALTER ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )? id=cident
|
||||
( mask=columnMask { $stmt.mask(id, mask); }
|
||||
| K_DROP K_MASKED { $stmt.mask(id, null); } )
|
||||
|
||||
| K_ADD ( K_IF K_NOT K_EXISTS { $stmt.ifColumnNotExists(true); } )?
|
||||
( id=ident v=comparatorType b=isStaticColumn { $stmt.add(id, v, b); }
|
||||
| ('(' id1=ident v1=comparatorType b1=isStaticColumn { $stmt.add(id1, v1, b1); }
|
||||
( ',' idn=ident vn=comparatorType bn=isStaticColumn { $stmt.add(idn, vn, bn); } )* ')') )
|
||||
( id=ident v=comparatorType b=isStaticColumn (m=columnMask)? { $stmt.add(id, v, b, m); }
|
||||
| ('(' id1=ident v1=comparatorType b1=isStaticColumn (m1=columnMask)? { $stmt.add(id1, v1, b1, m1); }
|
||||
( ',' idn=ident vn=comparatorType bn=isStaticColumn (mn=columnMask)? { $stmt.add(idn, vn, bn, mn); mn=null; } )* ')') )
|
||||
|
||||
| K_DROP ( K_IF K_EXISTS { $stmt.ifColumnExists(true); } )?
|
||||
( id=ident { $stmt.drop(id); }
|
||||
|
|
@ -1112,7 +1128,7 @@ listPermissionsStatement returns [ListPermissionsStatement stmt]
|
|||
;
|
||||
|
||||
permission returns [Permission perm]
|
||||
: p=(K_CREATE | K_ALTER | K_DROP | K_SELECT | K_MODIFY | K_AUTHORIZE | K_DESCRIBE | K_EXECUTE)
|
||||
: p=(K_CREATE | K_ALTER | K_DROP | K_SELECT | K_MODIFY | K_AUTHORIZE | K_DESCRIBE | K_EXECUTE | K_UNMASK | K_SELECT_MASKED)
|
||||
{ $perm = Permission.valueOf($p.text.toUpperCase()); }
|
||||
;
|
||||
|
||||
|
|
@ -1940,5 +1956,8 @@ basic_unreserved_keyword returns [String str]
|
|||
| K_MBEANS
|
||||
| K_REPLACE
|
||||
| K_UNSET
|
||||
| K_MASKED
|
||||
| K_UNMASK
|
||||
| K_SELECT_MASKED
|
||||
) { $str = $k.text; }
|
||||
;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public class CassandraRoleManager implements IRoleManager
|
|||
private static final Logger logger = LoggerFactory.getLogger(CassandraRoleManager.class);
|
||||
|
||||
public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
|
||||
static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
|
||||
public static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
|
||||
|
||||
/**
|
||||
* We need to treat the default superuser as a special case since during initial node startup, we may end up with
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ public class DataResource implements IResource
|
|||
Permission.DROP,
|
||||
Permission.SELECT,
|
||||
Permission.MODIFY,
|
||||
Permission.AUTHORIZE);
|
||||
Permission.AUTHORIZE,
|
||||
Permission.UNMASK,
|
||||
Permission.SELECT_MASKED);
|
||||
|
||||
// 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,
|
||||
|
|
@ -54,7 +56,9 @@ public class DataResource implements IResource
|
|||
Permission.DROP,
|
||||
Permission.SELECT,
|
||||
Permission.MODIFY,
|
||||
Permission.AUTHORIZE);
|
||||
Permission.AUTHORIZE,
|
||||
Permission.UNMASK,
|
||||
Permission.SELECT_MASKED);
|
||||
|
||||
// permissions which may be granted on one or all keyspaces
|
||||
private static final Set<Permission> KEYSPACE_LEVEL_PERMISSIONS = Sets.immutableEnumSet(Permission.CREATE,
|
||||
|
|
@ -62,7 +66,9 @@ public class DataResource implements IResource
|
|||
Permission.DROP,
|
||||
Permission.SELECT,
|
||||
Permission.MODIFY,
|
||||
Permission.AUTHORIZE);
|
||||
Permission.AUTHORIZE,
|
||||
Permission.UNMASK,
|
||||
Permission.SELECT_MASKED);
|
||||
private static final String ROOT_NAME = "data";
|
||||
private static final DataResource ROOT_RESOURCE = new DataResource(Level.ROOT, null, null);
|
||||
|
||||
|
|
|
|||
|
|
@ -61,9 +61,13 @@ public enum Permission
|
|||
DESCRIBE, // required on the root-level RoleResource to list all Roles
|
||||
|
||||
// UDF permissions
|
||||
EXECUTE; // required to invoke any user defined function or aggregate
|
||||
EXECUTE, // required to invoke any user defined function or aggregate
|
||||
|
||||
UNMASK, // required to see masked data
|
||||
|
||||
SELECT_MASKED; // required for SELECT on a table with restictions on masked columns
|
||||
|
||||
public static final Set<Permission> ALL =
|
||||
Sets.immutableEnumSet(EnumSet.range(Permission.CREATE, Permission.EXECUTE));
|
||||
Sets.immutableEnumSet(EnumSet.range(Permission.CREATE, Permission.SELECT_MASKED));
|
||||
public static final Set<Permission> NONE = ImmutableSet.of();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -618,6 +618,8 @@ public class Config
|
|||
*/
|
||||
public boolean allow_extra_insecure_udfs = false;
|
||||
|
||||
public boolean dynamic_data_masking_enabled = false;
|
||||
|
||||
/**
|
||||
* Time in milliseconds after a warning will be emitted to the log and to the client that a UDF runs too long.
|
||||
* (Only valid, if user_defined_functions_threads_enabled==true)
|
||||
|
|
|
|||
|
|
@ -4676,4 +4676,18 @@ public class DatabaseDescriptor
|
|||
{
|
||||
return Objects.requireNonNull(sstableFormatFactories, "Forgot to initialize DatabaseDescriptor?");
|
||||
}
|
||||
|
||||
public static boolean getDynamicDataMaskingEnabled()
|
||||
{
|
||||
return conf.dynamic_data_masking_enabled;
|
||||
}
|
||||
|
||||
public static void setDynamicDataMaskingEnabled(boolean enabled)
|
||||
{
|
||||
if (enabled != conf.dynamic_data_masking_enabled)
|
||||
{
|
||||
logger.info("Setting dynamic_data_masking_enabled to {}", enabled);
|
||||
conf.dynamic_data_masking_enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ public class QueryProcessor implements QueryHandler
|
|||
.map(m -> MessagingService.instance().<ReadResponse>sendWithResult(m, address))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
ResultSetBuilder result = new ResultSetBuilder(select.getResultMetadata(), select.getSelection().newSelectors(options), null);
|
||||
ResultSetBuilder result = new ResultSetBuilder(select.getResultMetadata(), select.getSelection().newSelectors(options), false);
|
||||
return future.map(list -> {
|
||||
int i = 0;
|
||||
for (Message<ReadResponse> m : list)
|
||||
|
|
@ -611,17 +611,19 @@ public class QueryProcessor implements QueryHandler
|
|||
return select.executeRawInternal(makeInternalOptionsWithNowInSec(prepared.statement, nowInSec, values), internalQueryState().getClientState(), nowInSec);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static UntypedResultSet resultify(String query, RowIterator partition)
|
||||
{
|
||||
return resultify(query, PartitionIterators.singletonIterator(partition));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static UntypedResultSet resultify(String query, PartitionIterator partitions)
|
||||
{
|
||||
try (PartitionIterator iter = partitions)
|
||||
{
|
||||
SelectStatement ss = (SelectStatement) getStatement(query, null);
|
||||
ResultSet cqlRows = ss.process(iter, FBUtilities.nowInSeconds());
|
||||
ResultSet cqlRows = ss.process(iter, FBUtilities.nowInSeconds(), true);
|
||||
return UntypedResultSet.create(cqlRows);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
|
|||
try (ReadExecutionController executionController = pager.executionController();
|
||||
PartitionIterator iter = pager.fetchPageInternal(pageSize, executionController))
|
||||
{
|
||||
currentPage = select.process(iter, nowInSec).rows.iterator();
|
||||
currentPage = select.process(iter, nowInSec, true).rows.iterator();
|
||||
}
|
||||
}
|
||||
return new Row(metadata, currentPage.next());
|
||||
|
|
@ -286,7 +286,7 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
|
|||
|
||||
try (PartitionIterator iter = pager.fetchPage(pageSize, cl, clientState, nanoTime()))
|
||||
{
|
||||
currentPage = select.process(iter, nowInSec).rows.iterator();
|
||||
currentPage = select.process(iter, nowInSec, true).rows.iterator();
|
||||
}
|
||||
}
|
||||
return new Row(metadata, currentPage.next());
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import org.apache.cassandra.cql3.ColumnSpecification;
|
|||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.schema.UserFunctions;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
|
||||
|
|
@ -68,7 +69,31 @@ public final class FunctionResolver
|
|||
AbstractType<?> receiverType)
|
||||
throws InvalidRequestException
|
||||
{
|
||||
Collection<Function> candidates = collectCandidates(keyspace, name, receiverKeyspace, receiverTable, providedArgs, receiverType);
|
||||
return get(keyspace, name, providedArgs, receiverKeyspace, receiverTable, receiverType, UserFunctions.none());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keyspace the current keyspace
|
||||
* @param name the name of the function
|
||||
* @param providedArgs the arguments provided for the function call
|
||||
* @param receiverKeyspace the receiver's keyspace
|
||||
* @param receiverTable the receiver's table
|
||||
* @param receiverType if the receiver type is known (during inserts, for example), this should be the type of
|
||||
* the receiver
|
||||
* @param functions a set of user functions that is not yet available in the schema, used during startup when those
|
||||
* functions might not be yet available
|
||||
*/
|
||||
@Nullable
|
||||
public static Function get(String keyspace,
|
||||
FunctionName name,
|
||||
List<? extends AssignmentTestable> providedArgs,
|
||||
String receiverKeyspace,
|
||||
String receiverTable,
|
||||
AbstractType<?> receiverType,
|
||||
UserFunctions functions)
|
||||
throws InvalidRequestException
|
||||
{
|
||||
Collection<Function> candidates = collectCandidates(keyspace, name, receiverKeyspace, receiverTable, providedArgs, receiverType, functions);
|
||||
|
||||
if (candidates.isEmpty())
|
||||
return null;
|
||||
|
|
@ -89,13 +114,15 @@ public final class FunctionResolver
|
|||
String receiverKeyspace,
|
||||
String receiverTable,
|
||||
List<? extends AssignmentTestable> providedArgs,
|
||||
AbstractType<?> receiverType)
|
||||
AbstractType<?> receiverType,
|
||||
UserFunctions functions)
|
||||
{
|
||||
Collection<Function> candidates = new ArrayList<>();
|
||||
|
||||
if (name.hasKeyspace())
|
||||
{
|
||||
// function name is fully qualified (keyspace + name)
|
||||
candidates.addAll(functions.get(name));
|
||||
candidates.addAll(Schema.instance.getUserFunctions(name));
|
||||
candidates.addAll(NativeFunctions.instance.getFunctions(name));
|
||||
candidates.addAll(NativeFunctions.instance.getFactories(name).stream()
|
||||
|
|
@ -107,7 +134,9 @@ public final class FunctionResolver
|
|||
{
|
||||
// function name is not fully qualified
|
||||
// add 'current keyspace' candidates
|
||||
candidates.addAll(Schema.instance.getUserFunctions(new FunctionName(keyspace, name.name)));
|
||||
FunctionName userName = new FunctionName(keyspace, name.name);
|
||||
candidates.addAll(functions.get(userName));
|
||||
candidates.addAll(Schema.instance.getUserFunctions(userName));
|
||||
// add 'SYSTEM' (native) candidates
|
||||
FunctionName nativeName = name.asNativeFunction();
|
||||
candidates.addAll(NativeFunctions.instance.getFunctions(nativeName));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,314 @@
|
|||
/*
|
||||
* 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 java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.AssignmentTestable;
|
||||
import org.apache.cassandra.cql3.CQL3Type;
|
||||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.CqlBuilder;
|
||||
import org.apache.cassandra.cql3.Term;
|
||||
import org.apache.cassandra.cql3.Terms;
|
||||
import org.apache.cassandra.cql3.functions.Function;
|
||||
import org.apache.cassandra.cql3.functions.FunctionName;
|
||||
import org.apache.cassandra.cql3.functions.FunctionResolver;
|
||||
import org.apache.cassandra.cql3.functions.ScalarFunction;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.ReversedType;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
|
||||
|
||||
/**
|
||||
* Dynamic data mask that can be applied to a schema column.
|
||||
* <p>
|
||||
* It consists on a partial application of a certain {@link MaskingFunction} to the values of a column, with the
|
||||
* precondition that the type of any masked column is compatible with the type of the first argument of the function.
|
||||
* <p>
|
||||
* This partial application is meant to be associated to specific columns in the schema, acting as a mask for the values
|
||||
* of those columns. It's associated to queries such as:
|
||||
* <pre>
|
||||
* CREATE TABLE t (k int PRIMARY KEY, v int MASKED WITH mask_inner(1, 1));
|
||||
* ALTER TABLE t ALTER v MASKED WITH mask_inner(2, 1);
|
||||
* ALTER TABLE t ALTER v DROP MASKED;
|
||||
* </pre>
|
||||
* Note that in the example above we are referencing the {@code mask_inner} function with two arguments. However, that
|
||||
* CQL function actually has three arguments. The first argument is always ommitted when attaching the function to a
|
||||
* schema column. The value of that first argument is always the value of the masked column, in this case an int.
|
||||
*/
|
||||
public abstract class ColumnMask
|
||||
{
|
||||
public static final String DISABLED_ERROR_MESSAGE = "Cannot mask columns because dynamic data masking is not " +
|
||||
"enabled. You can enable it with the " +
|
||||
"dynamic_data_masking_enabled property on cassandra.yaml";
|
||||
|
||||
/** The CQL function used for masking. */
|
||||
public final ScalarFunction function;
|
||||
|
||||
/** The values of the arguments of the partially applied masking function. */
|
||||
protected final ByteBuffer[] partialArgumentValues;
|
||||
|
||||
private ColumnMask(ScalarFunction function, ByteBuffer... partialArgumentValues)
|
||||
{
|
||||
assert function.argTypes().size() == partialArgumentValues.length + 1;
|
||||
this.function = function;
|
||||
this.partialArgumentValues = partialArgumentValues;
|
||||
}
|
||||
|
||||
public static ColumnMask build(ScalarFunction function, ByteBuffer... partialArgumentValues)
|
||||
{
|
||||
return function.isNative()
|
||||
? new Native((MaskingFunction) function, partialArgumentValues)
|
||||
: new Custom(function, partialArgumentValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The types of the arguments of the partially applied masking function, as an unmodifiable list.
|
||||
*/
|
||||
public List<AbstractType<?>> partialArgumentTypes()
|
||||
{
|
||||
List<AbstractType<?>> argTypes = function.argTypes();
|
||||
return argTypes.size() == 1
|
||||
? Collections.emptyList()
|
||||
: Collections.unmodifiableList(argTypes.subList(1, argTypes.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The values of the arguments of the partially applied masking function, as an unmodifiable list that can
|
||||
* contain nulls.
|
||||
*/
|
||||
public List<ByteBuffer> partialArgumentValues()
|
||||
{
|
||||
return Collections.unmodifiableList(Arrays.asList(partialArgumentValues));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A copy of this mask for a version of its masked column that has its type reversed.
|
||||
*/
|
||||
public ColumnMask withReversedType()
|
||||
{
|
||||
AbstractType<?> reversed = ReversedType.getInstance(function.argTypes().get(0));
|
||||
List<AbstractType<?>> args = ImmutableList.<AbstractType<?>>builder()
|
||||
.add(reversed)
|
||||
.addAll(partialArgumentTypes())
|
||||
.build();
|
||||
Function newFunction = FunctionResolver.get(function.name().keyspace, function.name(), args, null, null, null);
|
||||
assert newFunction != null;
|
||||
return build((ScalarFunction) newFunction, partialArgumentValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param protocolVersion the used version of the transport protocol
|
||||
* @param value a column value to be masked
|
||||
* @return the specified value after having been masked by the masked function
|
||||
*/
|
||||
public ByteBuffer mask(ProtocolVersion protocolVersion, ByteBuffer value)
|
||||
{
|
||||
if (!DatabaseDescriptor.getDynamicDataMaskingEnabled())
|
||||
return value;
|
||||
|
||||
return maskInternal(protocolVersion, value);
|
||||
}
|
||||
|
||||
protected abstract ByteBuffer maskInternal(ProtocolVersion protocolVersion, ByteBuffer value);
|
||||
|
||||
public static void ensureEnabled()
|
||||
{
|
||||
if (!DatabaseDescriptor.getDynamicDataMaskingEnabled())
|
||||
throw new InvalidRequestException(DISABLED_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
ColumnMask mask = (ColumnMask) o;
|
||||
return function.name().equals(mask.function.name())
|
||||
&& Arrays.equals(partialArgumentValues, mask.partialArgumentValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(function.name(), Arrays.hashCode(partialArgumentValues));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
List<AbstractType<?>> types = partialArgumentTypes();
|
||||
List<String> arguments = new ArrayList<>(types.size());
|
||||
for (int i = 0; i < types.size(); i++)
|
||||
{
|
||||
CQL3Type type = types.get(i).asCQL3Type();
|
||||
ByteBuffer value = partialArgumentValues[i];
|
||||
arguments.add(type.toCQLLiteral(value, ProtocolVersion.CURRENT));
|
||||
}
|
||||
return format("%s(%s)", function.name(), StringUtils.join(arguments, ", "));
|
||||
}
|
||||
|
||||
public void appendCqlTo(CqlBuilder builder)
|
||||
{
|
||||
builder.append(" MASKED WITH ").append(toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ColumnMask} for native masking functions.
|
||||
*/
|
||||
private static class Native extends ColumnMask
|
||||
{
|
||||
private final MaskingFunction.Masker masker;
|
||||
|
||||
public Native(MaskingFunction function, ByteBuffer... partialArgumentValues)
|
||||
{
|
||||
super(function, partialArgumentValues);
|
||||
masker = function.masker(partialArgumentValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuffer maskInternal(ProtocolVersion protocolVersion, ByteBuffer value)
|
||||
{
|
||||
return masker.mask(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ColumnMask} for user-defined masking functions.
|
||||
*/
|
||||
private static class Custom extends ColumnMask
|
||||
{
|
||||
public Custom(ScalarFunction function, ByteBuffer... partialArgumentValues)
|
||||
{
|
||||
super(function, partialArgumentValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuffer maskInternal(ProtocolVersion protocolVersion, ByteBuffer value)
|
||||
{
|
||||
List<ByteBuffer> argumentValues;
|
||||
int numPartialArgs = partialArgumentValues.length;
|
||||
if (numPartialArgs == 0)
|
||||
{
|
||||
argumentValues = Collections.singletonList(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ByteBuffer[] args = new ByteBuffer[numPartialArgs + 1];
|
||||
args[0] = value;
|
||||
System.arraycopy(partialArgumentValues, 0, args, 1, numPartialArgs);
|
||||
argumentValues = Arrays.asList(args);
|
||||
}
|
||||
|
||||
return function.execute(protocolVersion, argumentValues);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parsed but not prepared column mask.
|
||||
*/
|
||||
public final static class Raw
|
||||
{
|
||||
public final FunctionName name;
|
||||
public final List<Term.Raw> rawPartialArguments;
|
||||
|
||||
public Raw(FunctionName name, List<Term.Raw> rawPartialArguments)
|
||||
{
|
||||
this.name = name;
|
||||
this.rawPartialArguments = rawPartialArguments;
|
||||
}
|
||||
|
||||
public ColumnMask prepare(String keyspace, String table, ColumnIdentifier column, AbstractType<?> type)
|
||||
{
|
||||
ScalarFunction function = findMaskingFunction(keyspace, table, column, type);
|
||||
ByteBuffer[] partialArguments = preparePartialArguments(keyspace, function);
|
||||
return ColumnMask.build(function, partialArguments);
|
||||
}
|
||||
|
||||
private ScalarFunction findMaskingFunction(String keyspace, String table, ColumnIdentifier column, AbstractType<?> type)
|
||||
{
|
||||
List<AssignmentTestable> args = new ArrayList<>(rawPartialArguments.size() + 1);
|
||||
args.add(type);
|
||||
args.addAll(rawPartialArguments);
|
||||
|
||||
Function function = FunctionResolver.get(keyspace, name, args, keyspace, table, type);
|
||||
|
||||
if (function == null)
|
||||
throw invalidRequest("Unable to find masking function for %s, " +
|
||||
"no declared function matches the signature %s",
|
||||
column, this);
|
||||
|
||||
if (function.isAggregate())
|
||||
throw invalidRequest("Aggregate function %s cannot be used for masking table columns", this);
|
||||
|
||||
if (function.isNative() && !(function instanceof MaskingFunction))
|
||||
throw invalidRequest("Not-masking function %s cannot be used for masking table columns", this);
|
||||
|
||||
if (!function.isNative() && !function.name().keyspace.equals(keyspace))
|
||||
throw invalidRequest("Masking function %s doesn't belong to the same keyspace as the table %s.%s",
|
||||
this, keyspace, table);
|
||||
|
||||
CQL3Type returnType = function.returnType().asCQL3Type();
|
||||
CQL3Type expectedType = type.asCQL3Type();
|
||||
if (!returnType.equals(expectedType))
|
||||
throw invalidRequest("Masking function %s return type is %s. " +
|
||||
"This is different to the type of the masked column %s of type %s. " +
|
||||
"Masking functions can only be attached to table columns " +
|
||||
"if they return the same data type as the masked column.",
|
||||
this, returnType, column, expectedType);
|
||||
|
||||
return (ScalarFunction) function;
|
||||
}
|
||||
|
||||
private ByteBuffer[] preparePartialArguments(String keyspace, ScalarFunction function)
|
||||
{
|
||||
// Note that there could be null arguments
|
||||
ByteBuffer[] arguments = new ByteBuffer[rawPartialArguments.size()];
|
||||
|
||||
for (int i = 0; i < rawPartialArguments.size(); i++)
|
||||
{
|
||||
String term = rawPartialArguments.get(i).toString();
|
||||
AbstractType<?> type = function.argTypes().get(i + 1);
|
||||
arguments[i] = Terms.asBytes(keyspace, term, type);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return format("%s(%s)", name, StringUtils.join(rawPartialArguments, ", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ import org.apache.cassandra.cql3.functions.FunctionName;
|
|||
import org.apache.cassandra.cql3.functions.FunctionParameter;
|
||||
import org.apache.cassandra.cql3.functions.NativeFunction;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* A {@link MaskingFunction} that returns a fixed replacement value for the data type of its single argument.
|
||||
|
|
@ -41,18 +40,34 @@ public class DefaultMaskingFunction extends MaskingFunction
|
|||
{
|
||||
public static final String NAME = "default";
|
||||
|
||||
private final ByteBuffer defaultValue;
|
||||
private final Masker masker;
|
||||
|
||||
private <T> DefaultMaskingFunction(FunctionName name, AbstractType<T> inputType)
|
||||
{
|
||||
super(name, inputType, inputType);
|
||||
this.defaultValue = inputType.getMaskedValue();
|
||||
masker = new Masker(inputType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
|
||||
public Masker masker(ByteBuffer... parameters)
|
||||
{
|
||||
return defaultValue;
|
||||
return masker;
|
||||
}
|
||||
|
||||
private static class Masker implements MaskingFunction.Masker
|
||||
{
|
||||
private final ByteBuffer defaultValue;
|
||||
|
||||
private Masker(AbstractType<?> inputType)
|
||||
{
|
||||
defaultValue = inputType.getMaskedValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer mask(ByteBuffer value)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return a {@link FunctionFactory} to build new {@link DefaultMaskingFunction}s. */
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import org.apache.cassandra.db.marshal.BytesType;
|
|||
import org.apache.cassandra.db.marshal.StringType;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
|
||||
/**
|
||||
|
|
@ -78,20 +77,34 @@ public class HashMaskingFunction extends MaskingFunction
|
|||
}
|
||||
|
||||
@Override
|
||||
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
|
||||
public Masker masker(ByteBuffer... parameters)
|
||||
{
|
||||
MessageDigest digest;
|
||||
if (algorithmArgumentType == null || parameters.get(1) == null)
|
||||
return new Masker(algorithmArgumentType, parameters);
|
||||
}
|
||||
|
||||
private static class Masker implements MaskingFunction.Masker
|
||||
{
|
||||
private final MessageDigest digest;
|
||||
|
||||
private Masker(StringType algorithmArgumentType, ByteBuffer... parameters)
|
||||
{
|
||||
digest = DEFAULT_DIGEST.get();
|
||||
}
|
||||
else
|
||||
{
|
||||
String algorithm = algorithmArgumentType.compose(parameters.get(1));
|
||||
digest = messageDigest(algorithm);
|
||||
if (algorithmArgumentType == null || parameters[0] == null)
|
||||
{
|
||||
digest = DEFAULT_DIGEST.get();
|
||||
}
|
||||
else
|
||||
{
|
||||
String algorithm = algorithmArgumentType.compose(parameters[0]);
|
||||
digest = messageDigest(algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer mask(ByteBuffer value)
|
||||
{
|
||||
return HashMaskingFunction.hash(digest, value);
|
||||
}
|
||||
|
||||
return hash(digest, parameters.get(0));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
|
||||
package org.apache.cassandra.cql3.functions.masking;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.ObjectArrays;
|
||||
|
||||
import org.apache.cassandra.cql3.functions.FunctionFactory;
|
||||
|
|
@ -25,6 +28,7 @@ import org.apache.cassandra.cql3.functions.FunctionName;
|
|||
import org.apache.cassandra.cql3.functions.FunctionParameter;
|
||||
import org.apache.cassandra.cql3.functions.NativeScalarFunction;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* A {@link NativeScalarFunction} that totally or partially replaces the original value of a column value,
|
||||
|
|
@ -52,6 +56,33 @@ public abstract class MaskingFunction extends NativeScalarFunction
|
|||
super(name.name, outputType, ObjectArrays.concat(inputType, argsType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
|
||||
{
|
||||
ByteBuffer[] partialParameters = new ByteBuffer[parameters.size() - 1];
|
||||
for (int i = 0; i < partialParameters.length; i++)
|
||||
partialParameters[i] = parameters.get(i + 1);
|
||||
|
||||
return masker(partialParameters).mask(parameters.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Masker} for the specified masking parameters.
|
||||
* This is meant to be used by {@link ColumnMask}, so it doesn't need to evaluate the arguments on every call.
|
||||
*
|
||||
* @param parameters the masking parameters in the function call.
|
||||
* @return a new {@link Masker} using the specified masking arguments
|
||||
*/
|
||||
public abstract Masker masker(ByteBuffer... parameters);
|
||||
|
||||
/**
|
||||
* Class that actually makes the masking of the first function parameter according to the masking arguments.
|
||||
*/
|
||||
public interface Masker
|
||||
{
|
||||
public ByteBuffer mask(ByteBuffer value);
|
||||
}
|
||||
|
||||
protected static abstract class Factory extends FunctionFactory
|
||||
{
|
||||
public Factory(String name, FunctionParameter... parameters)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import org.apache.cassandra.cql3.functions.FunctionName;
|
|||
import org.apache.cassandra.cql3.functions.FunctionParameter;
|
||||
import org.apache.cassandra.cql3.functions.NativeFunction;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* A {@link MaskingFunction} that always returns a {@code null} column. The returned value is always an absent column,
|
||||
|
|
@ -38,6 +37,7 @@ import org.apache.cassandra.transport.ProtocolVersion;
|
|||
public class NullMaskingFunction extends MaskingFunction
|
||||
{
|
||||
public static final String NAME = "null";
|
||||
private static final Masker MASKER = new Masker();
|
||||
|
||||
private NullMaskingFunction(FunctionName name, AbstractType<?> inputType)
|
||||
{
|
||||
|
|
@ -45,9 +45,18 @@ public class NullMaskingFunction extends MaskingFunction
|
|||
}
|
||||
|
||||
@Override
|
||||
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
|
||||
public Masker masker(ByteBuffer... parameters)
|
||||
{
|
||||
return null;
|
||||
return MASKER;
|
||||
}
|
||||
|
||||
private static class Masker implements MaskingFunction.Masker
|
||||
{
|
||||
@Override
|
||||
public ByteBuffer mask(ByteBuffer value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return a {@link FunctionFactory} to build new {@link NullMaskingFunction}s. */
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import org.apache.cassandra.db.marshal.AbstractType;
|
|||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* A {@link MaskingFunction} applied to a {@link org.apache.cassandra.db.marshal.StringType} value that,
|
||||
|
|
@ -88,37 +87,53 @@ public class PartialMaskingFunction extends MaskingFunction
|
|||
}
|
||||
|
||||
@Override
|
||||
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
|
||||
public Masker masker(ByteBuffer... parameters)
|
||||
{
|
||||
// Parse the beginning and end positions. No validation is needed since the masker accepts negatives,
|
||||
// but we should consider that the arguments migh be null.
|
||||
int begin = parameters.get(1) == null ? 0 : Int32Type.instance.compose(parameters.get(1));
|
||||
int end = parameters.get(2) == null ? 0 : Int32Type.instance.compose(parameters.get(2));
|
||||
return new Masker(parameters);
|
||||
}
|
||||
|
||||
// Parse the padding character. The type of the argument is a string of any length because we don't have a
|
||||
// character type in CQL, so we should verify that the passed string argument is single-character.
|
||||
char padding = DEFAULT_PADDING_CHAR;
|
||||
if (hasPaddingArgument && parameters.get(3) != null)
|
||||
private class Masker implements MaskingFunction.Masker
|
||||
{
|
||||
private final int begin, end;
|
||||
private final char padding;
|
||||
|
||||
private Masker(ByteBuffer... parameters)
|
||||
{
|
||||
String parameter = UTF8Type.instance.compose(parameters.get(3));
|
||||
if (parameter.length() != 1)
|
||||
// Parse the beginning and end positions. No validation is needed since the masker accepts negatives,
|
||||
// but we should consider that the arguments migh be null.
|
||||
begin = parameters[0] == null ? 0 : Int32Type.instance.compose(parameters[0]);
|
||||
end = parameters[1] == null ? 0 : Int32Type.instance.compose(parameters[1]);
|
||||
|
||||
// Parse the padding character. The type of the argument is a string of any length because we don't have a
|
||||
// character type in CQL, so we should verify that the passed string argument is single-character.
|
||||
if (hasPaddingArgument && parameters[2] != null)
|
||||
{
|
||||
throw new InvalidRequestException(String.format("The padding argument for function %s should " +
|
||||
"be single-character, but '%s' has %d characters.",
|
||||
name(), parameter, parameter.length()));
|
||||
String parameter = UTF8Type.instance.compose(parameters[2]);
|
||||
if (parameter.length() != 1)
|
||||
{
|
||||
throw new InvalidRequestException(String.format("The padding argument for function %s should " +
|
||||
"be single-character, but '%s' has %d characters.",
|
||||
name(), parameter, parameter.length()));
|
||||
}
|
||||
padding = parameter.charAt(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
padding = DEFAULT_PADDING_CHAR;
|
||||
}
|
||||
padding = parameter.charAt(0);
|
||||
}
|
||||
|
||||
// Null column values aren't masked
|
||||
ByteBuffer value = parameters.get(0);
|
||||
if (value == null)
|
||||
return null;
|
||||
@Override
|
||||
public ByteBuffer mask(ByteBuffer value)
|
||||
{
|
||||
// Null column values aren't masked
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
// We mask the string representation of the column value, even if the type of the column is not a string.
|
||||
String stringValue = inputType.compose(value);
|
||||
String maskedValue = type.mask(stringValue, begin, end, padding);
|
||||
return inputType.decompose(maskedValue);
|
||||
String stringValue = inputType.compose(value);
|
||||
String maskedValue = type.mask(stringValue, begin, end, padding);
|
||||
return inputType.decompose(maskedValue);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Type
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import org.apache.cassandra.cql3.functions.FunctionName;
|
|||
import org.apache.cassandra.cql3.functions.FunctionParameter;
|
||||
import org.apache.cassandra.cql3.functions.NativeFunction;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
/**
|
||||
* A {@link MaskingFunction} that replaces the specified column value by a certain replacement value.
|
||||
|
|
@ -45,9 +44,25 @@ public class ReplaceMaskingFunction extends MaskingFunction
|
|||
}
|
||||
|
||||
@Override
|
||||
public final ByteBuffer execute(ProtocolVersion protocolVersion, List<ByteBuffer> parameters)
|
||||
public Masker masker(ByteBuffer... parameters)
|
||||
{
|
||||
return parameters.get(1);
|
||||
return new Masker(parameters[0]);
|
||||
}
|
||||
|
||||
private static class Masker implements MaskingFunction.Masker
|
||||
{
|
||||
private final ByteBuffer replacement;
|
||||
|
||||
private Masker(ByteBuffer replacement)
|
||||
{
|
||||
this.replacement = replacement;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer mask(ByteBuffer value)
|
||||
{
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return a {@link FunctionFactory} to build new {@link ReplaceMaskingFunction}s. */
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ public final class ResultSetBuilder
|
|||
*/
|
||||
private final GroupMaker groupMaker;
|
||||
|
||||
/**
|
||||
* Whether masked columns should be unmasked.
|
||||
*/
|
||||
private final boolean unmask;
|
||||
|
||||
/*
|
||||
* We'll build CQL3 row one by one.
|
||||
*/
|
||||
|
|
@ -55,16 +60,17 @@ public final class ResultSetBuilder
|
|||
private long size = 0;
|
||||
private boolean sizeWarningEmitted = false;
|
||||
|
||||
public ResultSetBuilder(ResultMetadata metadata, Selectors selectors)
|
||||
public ResultSetBuilder(ResultMetadata metadata, Selectors selectors, boolean unmask)
|
||||
{
|
||||
this(metadata, selectors, null);
|
||||
this(metadata, selectors, unmask, null);
|
||||
}
|
||||
|
||||
public ResultSetBuilder(ResultMetadata metadata, Selectors selectors, GroupMaker groupMaker)
|
||||
public ResultSetBuilder(ResultMetadata metadata, Selectors selectors, boolean unmask, GroupMaker groupMaker)
|
||||
{
|
||||
this.resultSet = new ResultSet(metadata.copy(), new ArrayList<List<ByteBuffer>>());
|
||||
this.resultSet = new ResultSet(metadata.copy(), new ArrayList<>());
|
||||
this.selectors = selectors;
|
||||
this.groupMaker = groupMaker;
|
||||
this.unmask = unmask;
|
||||
}
|
||||
|
||||
private void addSize(List<ByteBuffer> row)
|
||||
|
|
@ -139,6 +145,7 @@ public final class ResultSetBuilder
|
|||
{
|
||||
inputRow = new Selector.InputRow(protocolVersion,
|
||||
columns,
|
||||
unmask,
|
||||
selectors.collectWritetimes(),
|
||||
selectors.collectTTLs());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ public interface Selectable extends AssignmentTestable
|
|||
*/
|
||||
public default boolean processesSelection()
|
||||
{
|
||||
// ColumnMetadata is the only case that returns false and override this
|
||||
// ColumnMetadata is the only case that returns false (if the column is not masked) and overrides this
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +322,7 @@ public interface Selectable extends AssignmentTestable
|
|||
@Override
|
||||
public WritetimeOrTTL prepare(TableMetadata table)
|
||||
{
|
||||
return new WritetimeOrTTL(column.prepare(table), selected.prepare(table), kind);
|
||||
return new WritetimeOrTTL((ColumnMetadata) column.prepare(table), selected.prepare(table), kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1240,12 +1240,17 @@ public interface Selectable extends AssignmentTestable
|
|||
this.quoted = quoted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnMetadata prepare(TableMetadata cfm)
|
||||
public ColumnMetadata columnMetadata(TableMetadata cfm)
|
||||
{
|
||||
return cfm.getExistingColumn(ColumnIdentifier.getInterned(text, quoted));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Selectable prepare(TableMetadata cfm)
|
||||
{
|
||||
return columnMetadata(cfm);
|
||||
}
|
||||
|
||||
public FieldIdentifier toFieldIdentifier()
|
||||
{
|
||||
return quoted ? FieldIdentifier.forQuoted(text)
|
||||
|
|
|
|||
|
|
@ -103,15 +103,20 @@ public abstract class Selection
|
|||
*/
|
||||
public Integer getOrderingIndex(ColumnMetadata c)
|
||||
{
|
||||
if (!isJson)
|
||||
return getResultSetIndex(c);
|
||||
|
||||
// If we order post-query in json, the first and only column that we ship to the client is the json column.
|
||||
// In that case, we should keep ordering columns around to perform the ordering, then these columns will
|
||||
// be placed after the json column. As a consequence of where the colums are placed, we should give the
|
||||
// ordering index a value based on their position in the json encoding and discard the original index.
|
||||
// (CASSANDRA-14286)
|
||||
return orderingColumns.indexOf(c) + 1;
|
||||
if (isJson)
|
||||
return orderingColumns.indexOf(c) + 1;
|
||||
|
||||
// If the column is masked it might appear twice, once masked in the selected column and once unmasked in
|
||||
// the ordering columns. For ordering we are interested in that second unmasked value.
|
||||
if (c.isMasked())
|
||||
return columns.lastIndexOf(c);
|
||||
|
||||
return getResultSetIndex(c);
|
||||
}
|
||||
|
||||
public ResultSet.ResultMetadata getResultMetadata()
|
||||
|
|
@ -133,15 +138,16 @@ public abstract class Selection
|
|||
return new SimpleSelection(table, all, Collections.emptySet(), true, isJson, returnStaticContentOnPartitionWithNoRows);
|
||||
}
|
||||
|
||||
public static Selection wildcardWithGroupBy(TableMetadata table,
|
||||
VariableSpecifications boundNames,
|
||||
boolean isJson,
|
||||
boolean returnStaticContentOnPartitionWithNoRows)
|
||||
public static Selection wildcardWithGroupByOrMaskedColumns(TableMetadata table,
|
||||
VariableSpecifications boundNames,
|
||||
Set<ColumnMetadata> orderingColumns,
|
||||
boolean isJson,
|
||||
boolean returnStaticContentOnPartitionWithNoRows)
|
||||
{
|
||||
return fromSelectors(table,
|
||||
Lists.newArrayList(table.allColumnsInSelectOrder()),
|
||||
boundNames,
|
||||
Collections.emptySet(),
|
||||
orderingColumns,
|
||||
Collections.emptySet(),
|
||||
true,
|
||||
isJson,
|
||||
|
|
@ -225,7 +231,7 @@ public abstract class Selection
|
|||
for (ColumnMetadata orderingColumn : orderingColumns)
|
||||
{
|
||||
int index = selectedColumns.indexOf(orderingColumn);
|
||||
if (index >= 0 && factories.indexOfSimpleSelectorFactory(index) >= 0)
|
||||
if (index >= 0 && factories.indexOfSimpleSelectorFactory(index) >= 0 && !orderingColumn.isMasked())
|
||||
continue;
|
||||
|
||||
filteredOrderingColumns.add(orderingColumn);
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@ public abstract class Selector
|
|||
{
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final List<ColumnMetadata> columns;
|
||||
private final boolean unmask;
|
||||
private final boolean collectWritetimes;
|
||||
private final boolean collectTTLs;
|
||||
|
||||
|
|
@ -314,18 +315,20 @@ public abstract class Selector
|
|||
private RowTimestamps ttls;
|
||||
private int index;
|
||||
|
||||
public InputRow(ProtocolVersion protocolVersion, List<ColumnMetadata> columns)
|
||||
public InputRow(ProtocolVersion protocolVersion, List<ColumnMetadata> columns, boolean unmask)
|
||||
{
|
||||
this(protocolVersion, columns, false, false);
|
||||
this(protocolVersion, columns, unmask, false, false);
|
||||
}
|
||||
|
||||
public InputRow(ProtocolVersion protocolVersion,
|
||||
List<ColumnMetadata> columns,
|
||||
boolean unmask,
|
||||
boolean collectWritetimes,
|
||||
boolean collectTTLs)
|
||||
{
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.columns = columns;
|
||||
this.unmask = unmask;
|
||||
this.collectWritetimes = collectWritetimes;
|
||||
this.collectTTLs = collectTTLs;
|
||||
|
||||
|
|
@ -347,6 +350,11 @@ public abstract class Selector
|
|||
return protocolVersion;
|
||||
}
|
||||
|
||||
public boolean unmask()
|
||||
{
|
||||
return unmask;
|
||||
}
|
||||
|
||||
public void add(ByteBuffer v)
|
||||
{
|
||||
values[index] = v;
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ final class SelectorFactories implements Iterable<Selector.Factory>
|
|||
*/
|
||||
public void addSelectorForOrdering(ColumnMetadata def, int index)
|
||||
{
|
||||
factories.add(SimpleSelector.newFactory(def, index));
|
||||
factories.add(SimpleSelector.newFactory(def, index, true));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import java.nio.ByteBuffer;
|
|||
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.cql3.ColumnSpecification;
|
||||
|
|
@ -45,7 +46,7 @@ public final class SimpleSelector extends Selector
|
|||
ByteBuffer columnName = ByteBufferUtil.readWithVIntLength(in);
|
||||
ColumnMetadata column = metadata.getColumn(columnName);
|
||||
int idx = in.readInt();
|
||||
return new SimpleSelector(column, idx);
|
||||
return new SimpleSelector(column, idx, false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -55,13 +56,14 @@ public final class SimpleSelector extends Selector
|
|||
public static final class SimpleSelectorFactory extends Factory
|
||||
{
|
||||
private final int idx;
|
||||
|
||||
private final ColumnMetadata column;
|
||||
private final boolean useForPostOrdering;
|
||||
|
||||
private SimpleSelectorFactory(int idx, ColumnMetadata def)
|
||||
private SimpleSelectorFactory(int idx, ColumnMetadata def, boolean useForPostOrdering)
|
||||
{
|
||||
this.idx = idx;
|
||||
this.column = def;
|
||||
this.useForPostOrdering = useForPostOrdering;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -84,7 +86,7 @@ public final class SimpleSelector extends Selector
|
|||
@Override
|
||||
public Selector newInstance(QueryOptions options)
|
||||
{
|
||||
return new SimpleSelector(column, idx);
|
||||
return new SimpleSelector(column, idx, useForPostOrdering);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -117,14 +119,15 @@ public final class SimpleSelector extends Selector
|
|||
|
||||
public final ColumnMetadata column;
|
||||
private final int idx;
|
||||
private final boolean useForPostOrdering;
|
||||
private ByteBuffer current;
|
||||
private ColumnTimestamps writetimes;
|
||||
private ColumnTimestamps ttls;
|
||||
private boolean isSet;
|
||||
|
||||
public static Factory newFactory(final ColumnMetadata def, final int idx)
|
||||
public static Factory newFactory(final ColumnMetadata def, final int idx, boolean useForPostOrdering)
|
||||
{
|
||||
return new SimpleSelectorFactory(idx, def);
|
||||
return new SimpleSelectorFactory(idx, def, useForPostOrdering);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -139,9 +142,18 @@ public final class SimpleSelector extends Selector
|
|||
if (!isSet)
|
||||
{
|
||||
isSet = true;
|
||||
current = input.getValue(idx);
|
||||
writetimes = input.getWritetimes(idx);
|
||||
ttls = input.getTtls(idx);
|
||||
|
||||
/*
|
||||
We apply the column mask of the column unless:
|
||||
- The column doesn't have a mask
|
||||
- This selector is for a query with ORDER BY post-ordering, indicated by this.useForPostOrdering
|
||||
- The input row is for a user with UNMASK permission, indicated by input.unmask()
|
||||
*/
|
||||
ColumnMask mask = useForPostOrdering || input.unmask() ? null : column.getMask();
|
||||
ByteBuffer value = input.getValue(idx);
|
||||
current = mask == null ? value : mask.mask(input.getProtocolVersion(), value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,11 +196,12 @@ public final class SimpleSelector extends Selector
|
|||
return column.name.toString();
|
||||
}
|
||||
|
||||
private SimpleSelector(ColumnMetadata column, int idx)
|
||||
private SimpleSelector(ColumnMetadata column, int idx, boolean useForPostOrdering)
|
||||
{
|
||||
super(Kind.SIMPLE_SELECTOR);
|
||||
this.column = column;
|
||||
this.idx = idx;
|
||||
this.useForPostOrdering = useForPostOrdering;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -228,6 +241,6 @@ public final class SimpleSelector extends Selector
|
|||
protected void serialize(DataOutputPlus out, int version) throws IOException
|
||||
{
|
||||
ByteBufferUtil.writeWithVIntLength(column.name.bytes, out);
|
||||
out.writeInt(idx);;
|
||||
out.writeInt(idx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -659,7 +659,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
|
|||
}
|
||||
|
||||
Selectors selectors = selection.newSelectors(options);
|
||||
ResultSetBuilder builder = new ResultSetBuilder(selection.getResultMetadata(), selectors);
|
||||
ResultSetBuilder builder = new ResultSetBuilder(selection.getResultMetadata(), selectors, false);
|
||||
SelectStatement.forSelection(metadata, selection)
|
||||
.processPartition(partition, options, builder, nowInSeconds);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import java.util.*;
|
|||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
|
@ -80,6 +82,7 @@ import org.apache.cassandra.utils.NoSpamLogger;
|
|||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
|
||||
import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull;
|
||||
import static org.apache.cassandra.cql3.statements.RequestValidations.checkNull;
|
||||
|
|
@ -96,7 +99,10 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime;
|
|||
* many of these are made accessible for the benefit of custom
|
||||
* QueryHandler implementations, so before reducing their accessibility
|
||||
* due consideration should be given.
|
||||
*
|
||||
* Note that select statements can be accessed by multiple threads, so we cannot rely on mutable attributes.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(SelectStatement.class);
|
||||
|
|
@ -236,6 +242,21 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
for (Function function : getFunctions())
|
||||
state.ensurePermission(Permission.EXECUTE, function);
|
||||
|
||||
if (!state.hasTablePermission(table, Permission.UNMASK) &&
|
||||
!state.hasTablePermission(table, Permission.SELECT_MASKED))
|
||||
{
|
||||
List<ColumnMetadata> queriedMaskedColumns = table.columns()
|
||||
.stream()
|
||||
.filter(ColumnMetadata::isMasked)
|
||||
.filter(restrictions::isRestricted)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!queriedMaskedColumns.isEmpty())
|
||||
throw new UnauthorizedException(format("User %s has no UNMASK nor SELECT_MASKED permission on table %s.%s, " +
|
||||
"cannot query masked columns %s",
|
||||
state.getUser().getName(), keyspace(), table(), queriedMaskedColumns));
|
||||
}
|
||||
}
|
||||
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
|
|
@ -256,6 +277,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
int userLimit = getLimit(options);
|
||||
int userPerPartitionLimit = getPerPartitionLimit(options);
|
||||
int pageSize = options.getPageSize();
|
||||
boolean unmask = !table.hasMaskedColumns() || state.getClientState().hasTablePermission(table, Permission.UNMASK);
|
||||
|
||||
Selectors selectors = selection.newSelectors(options);
|
||||
AggregationSpecification aggregationSpec = getAggregationSpec(options);
|
||||
|
|
@ -268,7 +290,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
|
||||
{
|
||||
rows = execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, null, queryStartNanoTime);
|
||||
rows = execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, null, queryStartNanoTime, unmask);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -282,7 +304,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
nowInSec,
|
||||
userLimit,
|
||||
aggregationSpec,
|
||||
queryStartNanoTime);
|
||||
queryStartNanoTime,
|
||||
unmask);
|
||||
}
|
||||
if (!SchemaConstants.isSystemKeyspace(table.keyspace))
|
||||
ClientRequestSizeMetrics.recordReadResponseMetrics(rows, restrictions, selection);
|
||||
|
|
@ -335,11 +358,12 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
int nowInSec,
|
||||
int userLimit,
|
||||
AggregationSpecification aggregationSpec,
|
||||
long queryStartNanoTime)
|
||||
long queryStartNanoTime,
|
||||
boolean unmask)
|
||||
{
|
||||
try (PartitionIterator data = query.execute(options.getConsistency(), state, queryStartNanoTime))
|
||||
{
|
||||
return processResults(data, options, selectors, nowInSec, userLimit, aggregationSpec);
|
||||
return processResults(data, options, selectors, nowInSec, userLimit, aggregationSpec, unmask);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -424,7 +448,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
int nowInSec,
|
||||
int userLimit,
|
||||
AggregationSpecification aggregationSpec,
|
||||
long queryStartNanoTime)
|
||||
long queryStartNanoTime,
|
||||
boolean unmask)
|
||||
{
|
||||
Guardrails.pageSize.guard(pageSize, table(), false, state.getClientState());
|
||||
|
||||
|
|
@ -453,7 +478,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
ResultMessage.Rows msg;
|
||||
try (PartitionIterator page = pager.fetchPage(pageSize, queryStartNanoTime))
|
||||
{
|
||||
msg = processResults(page, options, selectors, nowInSec, userLimit, aggregationSpec);
|
||||
msg = processResults(page, options, selectors, nowInSec, userLimit, aggregationSpec, unmask);
|
||||
}
|
||||
|
||||
// Please note that the isExhausted state of the pager only gets updated when we've closed the page, so this
|
||||
|
|
@ -475,9 +500,10 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
Selectors selectors,
|
||||
int nowInSec,
|
||||
int userLimit,
|
||||
AggregationSpecification aggregationSpec) throws RequestValidationException
|
||||
AggregationSpecification aggregationSpec,
|
||||
boolean unmask) throws RequestValidationException
|
||||
{
|
||||
ResultSet rset = process(partitions, options, selectors, nowInSec, userLimit, aggregationSpec);
|
||||
ResultSet rset = process(partitions, options, selectors, nowInSec, userLimit, aggregationSpec, unmask);
|
||||
return new ResultMessage.Rows(rset);
|
||||
}
|
||||
|
||||
|
|
@ -494,6 +520,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
int userLimit = getLimit(options);
|
||||
int userPerPartitionLimit = getPerPartitionLimit(options);
|
||||
int pageSize = options.getPageSize();
|
||||
boolean unmask = state.getClientState().hasTablePermission(table, Permission.UNMASK);
|
||||
|
||||
Selectors selectors = selection.newSelectors(options);
|
||||
AggregationSpecification aggregationSpec = getAggregationSpec(options);
|
||||
|
|
@ -512,7 +539,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
{
|
||||
try (PartitionIterator data = query.executeInternal(executionController))
|
||||
{
|
||||
return processResults(data, options, selectors, nowInSec, userLimit, null);
|
||||
return processResults(data, options, selectors, nowInSec, userLimit, null, unmask);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -526,7 +553,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
nowInSec,
|
||||
userLimit,
|
||||
aggregationSpec,
|
||||
queryStartNanoTime);
|
||||
queryStartNanoTime,
|
||||
unmask);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -584,11 +612,11 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
}
|
||||
}
|
||||
|
||||
public ResultSet process(PartitionIterator partitions, int nowInSec) throws InvalidRequestException
|
||||
public ResultSet process(PartitionIterator partitions, int nowInSec, boolean unmask) throws InvalidRequestException
|
||||
{
|
||||
QueryOptions options = QueryOptions.DEFAULT;
|
||||
Selectors selectors = selection.newSelectors(options);
|
||||
return process(partitions, options, selectors, nowInSec, getLimit(options), getAggregationSpec(options));
|
||||
return process(partitions, options, selectors, nowInSec, getLimit(options), getAggregationSpec(options), unmask);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -894,10 +922,11 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
Selectors selectors,
|
||||
int nowInSec,
|
||||
int userLimit,
|
||||
AggregationSpecification aggregationSpec) throws InvalidRequestException
|
||||
AggregationSpecification aggregationSpec,
|
||||
boolean unmask) throws InvalidRequestException
|
||||
{
|
||||
GroupMaker groupMaker = aggregationSpec == null ? null : aggregationSpec.newGroupMaker();
|
||||
ResultSetBuilder result = new ResultSetBuilder(getResultMetadata(), selectors, groupMaker);
|
||||
ResultSetBuilder result = new ResultSetBuilder(getResultMetadata(), selectors, unmask, groupMaker);
|
||||
|
||||
while (partitions.hasNext())
|
||||
{
|
||||
|
|
@ -1170,10 +1199,14 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
if (hasGroupBy)
|
||||
Guardrails.groupByEnabled.ensureEnabled(state);
|
||||
|
||||
boolean isJson = parameters.isJson;
|
||||
boolean returnStaticContentOnPartitionWithNoRows = restrictions.returnStaticContentOnPartitionWithNoRows();
|
||||
|
||||
if (selectables.isEmpty()) // wildcard query
|
||||
{
|
||||
return hasGroupBy ? Selection.wildcardWithGroupBy(table, boundNames, parameters.isJson, restrictions.returnStaticContentOnPartitionWithNoRows())
|
||||
: Selection.wildcard(table, parameters.isJson, restrictions.returnStaticContentOnPartitionWithNoRows());
|
||||
return hasGroupBy || table.hasMaskedColumns()
|
||||
? Selection.wildcardWithGroupByOrMaskedColumns(table, boundNames, resultSetOrderingColumns, isJson, returnStaticContentOnPartitionWithNoRows)
|
||||
: Selection.wildcard(table, isJson, returnStaticContentOnPartitionWithNoRows);
|
||||
}
|
||||
|
||||
return Selection.fromSelectors(table,
|
||||
|
|
@ -1182,8 +1215,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
resultSetOrderingColumns,
|
||||
restrictions.nonPKRestrictedColumns(false),
|
||||
hasGroupBy,
|
||||
parameters.isJson,
|
||||
restrictions.returnStaticContentOnPartitionWithNoRows());
|
||||
isJson,
|
||||
returnStaticContentOnPartitionWithNoRows);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1397,8 +1430,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
if (!restrictions.keyIsInRelation())
|
||||
return null;
|
||||
|
||||
List<Integer> idToSort = new ArrayList<Integer>(orderingColumns.size());
|
||||
List<Comparator<ByteBuffer>> sorters = new ArrayList<Comparator<ByteBuffer>>(orderingColumns.size());
|
||||
List<Integer> idToSort = new ArrayList<>(orderingColumns.size());
|
||||
List<Comparator<ByteBuffer>> sorters = new ArrayList<>(orderingColumns.size());
|
||||
|
||||
for (ColumnMetadata orderingColumn : orderingColumns.keySet())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
|
|
@ -40,6 +43,7 @@ import org.apache.cassandra.cql3.CQL3Type;
|
|||
import org.apache.cassandra.cql3.CQLStatement;
|
||||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.QualifiedName;
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
|
|
@ -158,6 +162,78 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE [IF EXISTS] <table> ALTER [IF EXISTS] <column> ( MASKED WITH <newMask> | DROP MASKED )
|
||||
*/
|
||||
public static class MaskColumn extends AlterTableStatement
|
||||
{
|
||||
private final ColumnIdentifier columnName;
|
||||
@Nullable
|
||||
private final ColumnMask.Raw rawMask;
|
||||
private final boolean ifColumnExists;
|
||||
|
||||
MaskColumn(String keyspaceName,
|
||||
String tableName,
|
||||
ColumnIdentifier columnName,
|
||||
@Nullable ColumnMask.Raw rawMask,
|
||||
boolean ifTableExists,
|
||||
boolean ifColumnExists)
|
||||
{
|
||||
super(keyspaceName, tableName, ifTableExists);
|
||||
this.columnName = columnName;
|
||||
this.rawMask = rawMask;
|
||||
this.ifColumnExists = ifColumnExists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ClientState state)
|
||||
{
|
||||
super.validate(state);
|
||||
|
||||
// we don't allow creating masks if they are disabled, but we still allow dropping them
|
||||
if (rawMask != null)
|
||||
ColumnMask.ensureEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
|
||||
{
|
||||
ColumnMetadata column = table.getColumn(columnName);
|
||||
|
||||
if (column == null)
|
||||
{
|
||||
if (!ifColumnExists)
|
||||
throw ire("Column with name '%s' doesn't exist on table '%s'", columnName, tableName);
|
||||
|
||||
return keyspace;
|
||||
}
|
||||
|
||||
ColumnMask oldMask = table.getColumn(columnName).getMask();
|
||||
ColumnMask newMask = rawMask == null ? null : rawMask.prepare(keyspace.name, table.name, columnName, column.type);
|
||||
|
||||
if (Objects.equals(oldMask, newMask))
|
||||
return keyspace;
|
||||
|
||||
TableMetadata.Builder tableBuilder = table.unbuild();
|
||||
tableBuilder.alterColumnMask(columnName, newMask);
|
||||
TableMetadata newTable = tableBuilder.build();
|
||||
newTable.validate();
|
||||
|
||||
// Update any reference on materialized views, so the mask is consistent among the base table and its views.
|
||||
Views.Builder viewsBuilder = keyspace.views.unbuild();
|
||||
for (ViewMetadata view : keyspace.views.forTable(table.id))
|
||||
{
|
||||
if (view.includes(columnName))
|
||||
{
|
||||
viewsBuilder.put(viewsBuilder.get(view.name()).withNewColumnMask(columnName, newMask));
|
||||
}
|
||||
}
|
||||
|
||||
return keyspace.withSwapped(keyspace.tables.withSwapped(newTable))
|
||||
.withSwapped(viewsBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE [IF EXISTS] <table> ADD [IF NOT EXISTS] <column> <newtype>
|
||||
* ALTER TABLE [IF EXISTS] <table> ADD [IF NOT EXISTS] (<column> <newtype>, <column1> <newtype1>, ... <columnn> <newtypen>)
|
||||
|
|
@ -169,12 +245,15 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
private final ColumnIdentifier name;
|
||||
private final CQL3Type.Raw type;
|
||||
private final boolean isStatic;
|
||||
@Nullable
|
||||
private final ColumnMask.Raw mask;
|
||||
|
||||
Column(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic)
|
||||
Column(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask)
|
||||
{
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.isStatic = isStatic;
|
||||
this.mask = mask;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,12 +267,6 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
this.ifColumnNotExists = ifColumnNotExists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ClientState state)
|
||||
{
|
||||
super.validate(state);
|
||||
}
|
||||
|
||||
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
|
||||
{
|
||||
Guardrails.alterTableEnabled.ensureEnabled("ALTER TABLE changing columns", state);
|
||||
|
|
@ -220,6 +293,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
ColumnIdentifier name = column.name;
|
||||
AbstractType<?> type = column.type.prepare(keyspaceName, keyspace.types).getType();
|
||||
boolean isStatic = column.isStatic;
|
||||
ColumnMask mask = column.mask == null ? null : column.mask.prepare(keyspaceName, tableName, name, type);
|
||||
|
||||
if (null != tableBuilder.getColumn(name)) {
|
||||
if (!ifColumnNotExists)
|
||||
|
|
@ -260,9 +334,9 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
}
|
||||
|
||||
if (isStatic)
|
||||
tableBuilder.addStaticColumn(name, type);
|
||||
tableBuilder.addStaticColumn(name, type, mask);
|
||||
else
|
||||
tableBuilder.addRegularColumn(name, type);
|
||||
tableBuilder.addRegularColumn(name, type, mask);
|
||||
|
||||
if (!isStatic)
|
||||
{
|
||||
|
|
@ -270,7 +344,8 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
{
|
||||
if (view.includeAllColumns)
|
||||
{
|
||||
ColumnMetadata viewColumn = ColumnMetadata.regularColumn(view.metadata, name.bytes, type);
|
||||
ColumnMetadata viewColumn = ColumnMetadata.regularColumn(view.metadata, name.bytes, type)
|
||||
.withNewMask(mask);
|
||||
viewsBuilder.put(viewsBuilder.get(view.name()).withAddedRegularColumn(viewColumn));
|
||||
}
|
||||
}
|
||||
|
|
@ -582,7 +657,13 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
{
|
||||
private enum Kind
|
||||
{
|
||||
ALTER_COLUMN, ADD_COLUMNS, DROP_COLUMNS, RENAME_COLUMNS, ALTER_OPTIONS, DROP_COMPACT_STORAGE
|
||||
ALTER_COLUMN,
|
||||
MASK_COLUMN,
|
||||
ADD_COLUMNS,
|
||||
DROP_COLUMNS,
|
||||
RENAME_COLUMNS,
|
||||
ALTER_OPTIONS,
|
||||
DROP_COMPACT_STORAGE
|
||||
}
|
||||
|
||||
private final QualifiedName name;
|
||||
|
|
@ -595,6 +676,10 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
// ADD
|
||||
private final List<AddColumns.Column> addedColumns = new ArrayList<>();
|
||||
|
||||
// ALTER MASK
|
||||
private ColumnIdentifier maskedColumn = null;
|
||||
private ColumnMask.Raw rawMask = null;
|
||||
|
||||
// DROP
|
||||
private final Set<ColumnIdentifier> droppedColumns = new HashSet<>();
|
||||
private Long timestamp = null; // will use execution timestamp if not provided by query
|
||||
|
|
@ -619,6 +704,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
switch (kind)
|
||||
{
|
||||
case ALTER_COLUMN: return new AlterColumn(keyspaceName, tableName, ifTableExists);
|
||||
case MASK_COLUMN: return new MaskColumn(keyspaceName, tableName, maskedColumn, rawMask, ifTableExists, ifColumnExists);
|
||||
case ADD_COLUMNS: return new AddColumns(keyspaceName, tableName, addedColumns, ifTableExists, ifColumnNotExists);
|
||||
case DROP_COLUMNS: return new DropColumns(keyspaceName, tableName, droppedColumns, ifTableExists, ifColumnExists, timestamp);
|
||||
case RENAME_COLUMNS: return new RenameColumns(keyspaceName, tableName, renamedColumns, ifTableExists, ifColumnExists);
|
||||
|
|
@ -634,10 +720,17 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
|
|||
kind = Kind.ALTER_COLUMN;
|
||||
}
|
||||
|
||||
public void add(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic)
|
||||
public void mask(ColumnIdentifier name, ColumnMask.Raw mask)
|
||||
{
|
||||
kind = Kind.MASK_COLUMN;
|
||||
maskedColumn = name;
|
||||
rawMask = mask;
|
||||
}
|
||||
|
||||
public void add(ColumnIdentifier name, CQL3Type.Raw type, boolean isStatic, @Nullable ColumnMask.Raw mask)
|
||||
{
|
||||
kind = Kind.ADD_COLUMNS;
|
||||
addedColumns.add(new AddColumns.Column(name, type, isStatic));
|
||||
addedColumns.add(new AddColumns.Column(name, type, isStatic, mask));
|
||||
}
|
||||
|
||||
public void drop(ColumnIdentifier name)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ package org.apache.cassandra.cql3.statements.schema;
|
|||
|
||||
import java.util.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
|
@ -33,6 +35,7 @@ import org.apache.cassandra.auth.IResource;
|
|||
import org.apache.cassandra.auth.Permission;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.*;
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||
import org.apache.cassandra.db.marshal.*;
|
||||
|
|
@ -54,7 +57,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
private static final Logger logger = LoggerFactory.getLogger(CreateTableStatement.class);
|
||||
private final String tableName;
|
||||
|
||||
private final Map<ColumnIdentifier, CQL3Type.Raw> rawColumns;
|
||||
private final Map<ColumnIdentifier, ColumnProperties.Raw> rawColumns;
|
||||
private final Set<ColumnIdentifier> staticColumns;
|
||||
private final List<ColumnIdentifier> partitionKeyColumns;
|
||||
private final List<ColumnIdentifier> clusteringColumns;
|
||||
|
|
@ -67,15 +70,12 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
|
||||
public CreateTableStatement(String keyspaceName,
|
||||
String tableName,
|
||||
|
||||
Map<ColumnIdentifier, CQL3Type.Raw> rawColumns,
|
||||
Map<ColumnIdentifier, ColumnProperties.Raw> rawColumns,
|
||||
Set<ColumnIdentifier> staticColumns,
|
||||
List<ColumnIdentifier> partitionKeyColumns,
|
||||
List<ColumnIdentifier> clusteringColumns,
|
||||
|
||||
LinkedHashMap<ColumnIdentifier, Boolean> clusteringOrder,
|
||||
TableAttributes attrs,
|
||||
|
||||
boolean ifNotExists,
|
||||
boolean useCompactStorage)
|
||||
{
|
||||
|
|
@ -151,6 +151,13 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
Guardrails.compactTablesEnabled.ensureEnabled(state);
|
||||
|
||||
validateDefaultTimeToLive(attrs.asNewTableParams());
|
||||
|
||||
// Verify that dynamic data masking is enabled if there are masked columns
|
||||
for (ColumnProperties.Raw raw : rawColumns.values())
|
||||
{
|
||||
if (raw.rawMask != null)
|
||||
ColumnMask.ensureEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
SchemaChange schemaChangeEvent(KeyspacesDiff diff)
|
||||
|
|
@ -186,15 +193,16 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
TableParams params = attrs.asNewTableParams();
|
||||
|
||||
// use a TreeMap to preserve ordering across JDK versions (see CASSANDRA-9492) - important for stable unit tests
|
||||
Map<ColumnIdentifier, CQL3Type> columns = new TreeMap<>(comparing(o -> o.bytes));
|
||||
rawColumns.forEach((column, type) -> columns.put(column, type.prepare(keyspaceName, types)));
|
||||
Map<ColumnIdentifier, ColumnProperties> columns = new TreeMap<>(comparing(o -> o.bytes));
|
||||
rawColumns.forEach((column, properties) -> columns.put(column, properties.prepare(keyspaceName, tableName, column, types)));
|
||||
|
||||
// check for nested non-frozen UDTs or collections in a non-frozen UDT
|
||||
columns.forEach((column, type) ->
|
||||
columns.forEach((column, properties) ->
|
||||
{
|
||||
if (type.isUDT() && type.getType().isMultiCell())
|
||||
AbstractType<?> type = properties.type;
|
||||
if (type.isUDT() && type.isMultiCell())
|
||||
{
|
||||
((UserType) type.getType()).fieldTypes().forEach(field ->
|
||||
((UserType) type).fieldTypes().forEach(field ->
|
||||
{
|
||||
if (field.isMultiCell())
|
||||
throw ire("Non-frozen UDTs with nested non-frozen collections are not supported");
|
||||
|
|
@ -209,45 +217,47 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
HashSet<ColumnIdentifier> primaryKeyColumns = new HashSet<>();
|
||||
concat(partitionKeyColumns, clusteringColumns).forEach(column ->
|
||||
{
|
||||
CQL3Type type = columns.get(column);
|
||||
if (null == type)
|
||||
ColumnProperties properties = columns.get(column);
|
||||
if (null == properties)
|
||||
throw ire("Unknown column '%s' referenced in PRIMARY KEY for table '%s'", column, tableName);
|
||||
|
||||
if (!primaryKeyColumns.add(column))
|
||||
throw ire("Duplicate column '%s' in PRIMARY KEY clause for table '%s'", column, tableName);
|
||||
|
||||
if (type.getType().isMultiCell())
|
||||
AbstractType<?> type = properties.type;
|
||||
if (type.isMultiCell())
|
||||
{
|
||||
CQL3Type cqlType = properties.cqlType;
|
||||
if (type.isCollection())
|
||||
throw ire("Invalid non-frozen collection type %s for PRIMARY KEY column '%s'", type, column);
|
||||
throw ire("Invalid non-frozen collection type %s for PRIMARY KEY column '%s'", cqlType, column);
|
||||
else
|
||||
throw ire("Invalid non-frozen user-defined type %s for PRIMARY KEY column '%s'", type, column);
|
||||
throw ire("Invalid non-frozen user-defined type %s for PRIMARY KEY column '%s'", cqlType, column);
|
||||
}
|
||||
|
||||
if (type.getType().isCounter())
|
||||
if (type.isCounter())
|
||||
throw ire("counter type is not supported for PRIMARY KEY column '%s'", column);
|
||||
|
||||
if (type.getType().referencesDuration())
|
||||
if (type.referencesDuration())
|
||||
throw ire("duration type is not supported for PRIMARY KEY column '%s'", column);
|
||||
|
||||
if (staticColumns.contains(column))
|
||||
throw ire("Static column '%s' cannot be part of the PRIMARY KEY", column);
|
||||
});
|
||||
|
||||
List<AbstractType<?>> partitionKeyTypes = new ArrayList<>();
|
||||
List<AbstractType<?>> clusteringTypes = new ArrayList<>();
|
||||
List<ColumnProperties> partitionKeyColumnProperties = new ArrayList<>();
|
||||
List<ColumnProperties> clusteringColumnProperties = new ArrayList<>();
|
||||
|
||||
partitionKeyColumns.forEach(column ->
|
||||
{
|
||||
CQL3Type type = columns.remove(column);
|
||||
partitionKeyTypes.add(type.getType());
|
||||
ColumnProperties columnProperties = columns.remove(column);
|
||||
partitionKeyColumnProperties.add(columnProperties);
|
||||
});
|
||||
|
||||
clusteringColumns.forEach(column ->
|
||||
{
|
||||
CQL3Type type = columns.remove(column);
|
||||
ColumnProperties columnProperties = columns.remove(column);
|
||||
boolean reverse = !clusteringOrder.getOrDefault(column, true);
|
||||
clusteringTypes.add(reverse ? ReversedType.getInstance(type.getType()) : type.getType());
|
||||
clusteringColumnProperties.add(reverse ? columnProperties.withReversedType() : columnProperties);
|
||||
});
|
||||
|
||||
if (clusteringOrder.size() > clusteringColumns.size())
|
||||
|
|
@ -270,7 +280,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
// For COMPACT STORAGE, we reject any "feature" that we wouldn't be able to translate back to thrift.
|
||||
if (useCompactStorage)
|
||||
{
|
||||
validateCompactTable(clusteringTypes, columns);
|
||||
validateCompactTable(clusteringColumnProperties, columns);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -283,12 +293,12 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
* Counter table validation
|
||||
*/
|
||||
|
||||
boolean hasCounters = rawColumns.values().stream().anyMatch(CQL3Type.Raw::isCounter);
|
||||
boolean hasCounters = rawColumns.values().stream().anyMatch(c -> c.rawType.isCounter());
|
||||
if (hasCounters)
|
||||
{
|
||||
// We've handled anything that is not a PRIMARY KEY so columns only contains NON-PK columns. So
|
||||
// if it's a counter table, make sure we don't have non-counter types
|
||||
if (columns.values().stream().anyMatch(t -> !t.getType().isCounter()))
|
||||
if (columns.values().stream().anyMatch(t -> !t.type.isCounter()))
|
||||
throw ire("Cannot mix counter and non counter columns in the same table");
|
||||
|
||||
if (params.defaultTimeToLive > 0)
|
||||
|
|
@ -308,38 +318,44 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
.params(params);
|
||||
|
||||
for (int i = 0; i < partitionKeyColumns.size(); i++)
|
||||
builder.addPartitionKeyColumn(partitionKeyColumns.get(i), partitionKeyTypes.get(i));
|
||||
{
|
||||
ColumnProperties properties = partitionKeyColumnProperties.get(i);
|
||||
builder.addPartitionKeyColumn(partitionKeyColumns.get(i), properties.type, properties.mask);
|
||||
}
|
||||
|
||||
for (int i = 0; i < clusteringColumns.size(); i++)
|
||||
builder.addClusteringColumn(clusteringColumns.get(i), clusteringTypes.get(i));
|
||||
{
|
||||
ColumnProperties properties = clusteringColumnProperties.get(i);
|
||||
builder.addClusteringColumn(clusteringColumns.get(i), properties.type, properties.mask);
|
||||
}
|
||||
|
||||
if (useCompactStorage)
|
||||
{
|
||||
fixupCompactTable(clusteringTypes, columns, hasCounters, builder);
|
||||
fixupCompactTable(clusteringColumnProperties, columns, hasCounters, builder);
|
||||
}
|
||||
else
|
||||
{
|
||||
columns.forEach((column, type) -> {
|
||||
columns.forEach((column, properties) -> {
|
||||
if (staticColumns.contains(column))
|
||||
builder.addStaticColumn(column, type.getType());
|
||||
builder.addStaticColumn(column, properties.type, properties.mask);
|
||||
else
|
||||
builder.addRegularColumn(column, type.getType());
|
||||
builder.addRegularColumn(column, properties.type, properties.mask);
|
||||
});
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void validateCompactTable(List<AbstractType<?>> clusteringTypes,
|
||||
Map<ColumnIdentifier, CQL3Type> columns)
|
||||
private void validateCompactTable(List<ColumnProperties> clusteringColumnProperties,
|
||||
Map<ColumnIdentifier, ColumnProperties> columns)
|
||||
{
|
||||
boolean isDense = !clusteringTypes.isEmpty();
|
||||
boolean isDense = !clusteringColumnProperties.isEmpty();
|
||||
|
||||
if (columns.values().stream().anyMatch(c -> c.getType().isMultiCell()))
|
||||
if (columns.values().stream().anyMatch(c -> c.type.isMultiCell()))
|
||||
throw ire("Non-frozen collections and UDTs are not supported with COMPACT STORAGE");
|
||||
if (!staticColumns.isEmpty())
|
||||
throw ire("Static columns are not supported in COMPACT STORAGE tables");
|
||||
|
||||
if (clusteringTypes.isEmpty())
|
||||
if (clusteringColumnProperties.isEmpty())
|
||||
{
|
||||
// It's a thrift "static CF" so there should be some columns definition
|
||||
if (columns.isEmpty())
|
||||
|
|
@ -361,8 +377,8 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
}
|
||||
}
|
||||
|
||||
private void fixupCompactTable(List<AbstractType<?>> clusteringTypes,
|
||||
Map<ColumnIdentifier, CQL3Type> columns,
|
||||
private void fixupCompactTable(List<ColumnProperties> clusteringTypes,
|
||||
Map<ColumnIdentifier, ColumnProperties> columns,
|
||||
boolean hasCounters,
|
||||
TableMetadata.Builder builder)
|
||||
{
|
||||
|
|
@ -381,12 +397,12 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
|
||||
builder.flags(flags);
|
||||
|
||||
columns.forEach((name, type) -> {
|
||||
columns.forEach((name, properties) -> {
|
||||
// Note that for "static" no-clustering compact storage we use static for the defined columns
|
||||
if (staticColumns.contains(name) || isStaticCompact)
|
||||
builder.addStaticColumn(name, type.getType());
|
||||
builder.addStaticColumn(name, properties.type, properties.mask);
|
||||
else
|
||||
builder.addRegularColumn(name, type.getType());
|
||||
builder.addRegularColumn(name, properties.type, properties.mask);
|
||||
});
|
||||
|
||||
DefaultNames names = new DefaultNames(builder.columnNames());
|
||||
|
|
@ -471,7 +487,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
private final boolean ifNotExists;
|
||||
|
||||
private boolean useCompactStorage = false;
|
||||
private final Map<ColumnIdentifier, CQL3Type.Raw> rawColumns = new HashMap<>();
|
||||
private final Map<ColumnIdentifier, ColumnProperties.Raw> rawColumns = new HashMap<>();
|
||||
private final Set<ColumnIdentifier> staticColumns = new HashSet<>();
|
||||
private final List<ColumnIdentifier> clusteringColumns = new ArrayList<>();
|
||||
|
||||
|
|
@ -495,15 +511,12 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
|
||||
return new CreateTableStatement(keyspaceName,
|
||||
name.getName(),
|
||||
|
||||
rawColumns,
|
||||
staticColumns,
|
||||
partitionKeyColumns,
|
||||
clusteringColumns,
|
||||
|
||||
clusteringOrder,
|
||||
attrs,
|
||||
|
||||
ifNotExists,
|
||||
useCompactStorage);
|
||||
}
|
||||
|
|
@ -524,9 +537,10 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
return name.getName();
|
||||
}
|
||||
|
||||
public void addColumn(ColumnIdentifier column, CQL3Type.Raw type, boolean isStatic)
|
||||
public void addColumn(ColumnIdentifier column, CQL3Type.Raw type, boolean isStatic, ColumnMask.Raw mask)
|
||||
{
|
||||
if (null != rawColumns.put(column, type))
|
||||
|
||||
if (null != rawColumns.put(column, new ColumnProperties.Raw(type, mask)))
|
||||
throw ire("Duplicate column '%s' declaration for table '%s'", column, name);
|
||||
|
||||
if (isStatic)
|
||||
|
|
@ -562,4 +576,52 @@ public final class CreateTableStatement extends AlterSchemaStatement
|
|||
throw ire("Duplicate column '%s' in CLUSTERING ORDER BY clause for table '%s'", column, name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class encapsulating the properties of a column, which are its type and mask.
|
||||
*/
|
||||
private final static class ColumnProperties
|
||||
{
|
||||
public final AbstractType<?> type;
|
||||
public final CQL3Type cqlType; // we keep the original CQL type for printing fully qualified user type names
|
||||
|
||||
@Nullable
|
||||
public final ColumnMask mask;
|
||||
|
||||
public ColumnProperties(AbstractType<?> type, CQL3Type cqlType, @Nullable ColumnMask mask)
|
||||
{
|
||||
this.type = type;
|
||||
this.cqlType = cqlType;
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
public ColumnProperties withReversedType()
|
||||
{
|
||||
return new ColumnProperties(ReversedType.getInstance(type),
|
||||
cqlType,
|
||||
mask == null ? null : mask.withReversedType());
|
||||
}
|
||||
|
||||
public final static class Raw
|
||||
{
|
||||
public final CQL3Type.Raw rawType;
|
||||
|
||||
@Nullable
|
||||
public final ColumnMask.Raw rawMask;
|
||||
|
||||
public Raw(CQL3Type.Raw rawType, @Nullable ColumnMask.Raw rawMask)
|
||||
{
|
||||
this.rawType = rawType;
|
||||
this.rawMask = rawMask;
|
||||
}
|
||||
|
||||
public ColumnProperties prepare(String keyspace, String table, ColumnIdentifier column, Types udts)
|
||||
{
|
||||
CQL3Type cqlType = rawType.prepare(keyspace, udts);
|
||||
AbstractType<?> type = cqlType.getType();
|
||||
ColumnMask mask = rawMask == null ? null : rawMask.prepare(keyspace, table, column, type);
|
||||
return new ColumnProperties(type, cqlType, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ public final class CreateViewStatement extends AlterSchemaStatement
|
|||
throw ire("Can only select columns by name when defining a materialized view (got %s)", selector.selectable);
|
||||
|
||||
// will throw IRE if the column doesn't exist in the base table
|
||||
ColumnMetadata column = (ColumnMetadata) selector.selectable.prepare(table);
|
||||
Selectable.RawIdentifier rawIdentifier = (Selectable.RawIdentifier) selector.selectable;
|
||||
ColumnMetadata column = rawIdentifier.columnMetadata(table);
|
||||
|
||||
selectedColumns.add(column.name);
|
||||
});
|
||||
|
|
@ -325,12 +326,18 @@ public final class CreateViewStatement extends AlterSchemaStatement
|
|||
builder.params(attrs.asNewTableParams())
|
||||
.kind(TableMetadata.Kind.VIEW);
|
||||
|
||||
partitionKeyColumns.forEach(name -> builder.addPartitionKeyColumn(name, getType(table, name)));
|
||||
clusteringColumns.forEach(name -> builder.addClusteringColumn(name, getType(table, name)));
|
||||
partitionKeyColumns.stream()
|
||||
.map(table::getColumn)
|
||||
.forEach(column -> builder.addPartitionKeyColumn(column.name, getType(column), column.getMask()));
|
||||
|
||||
clusteringColumns.stream()
|
||||
.map(table::getColumn)
|
||||
.forEach(column -> builder.addClusteringColumn(column.name, getType(column), column.getMask()));
|
||||
|
||||
selectedColumns.stream()
|
||||
.filter(name -> !primaryKeyColumns.contains(name))
|
||||
.forEach(name -> builder.addRegularColumn(name, getType(table, name)));
|
||||
.map(table::getColumn)
|
||||
.forEach(column -> builder.addRegularColumn(column.name, getType(column), column.getMask()));
|
||||
|
||||
ViewMetadata view = new ViewMetadata(table.id, table.name, rawColumns.isEmpty(), whereClause, builder.build());
|
||||
view.metadata.validate();
|
||||
|
|
@ -348,15 +355,15 @@ public final class CreateViewStatement extends AlterSchemaStatement
|
|||
client.ensureTablePermission(keyspaceName, tableName, Permission.ALTER);
|
||||
}
|
||||
|
||||
private AbstractType<?> getType(TableMetadata table, ColumnIdentifier name)
|
||||
private AbstractType<?> getType(ColumnMetadata column)
|
||||
{
|
||||
AbstractType<?> type = table.getColumn(name).type;
|
||||
if (clusteringOrder.containsKey(name))
|
||||
AbstractType<?> type = column.type;
|
||||
if (clusteringOrder.containsKey(column.name))
|
||||
{
|
||||
boolean reverse = !clusteringOrder.get(name);
|
||||
boolean reverse = !clusteringOrder.get(column.name);
|
||||
|
||||
if (type.isReversed() && !reverse)
|
||||
return ((ReversedType) type).baseType;
|
||||
return ((ReversedType<?>) type).baseType;
|
||||
|
||||
if (!type.isReversed() && reverse)
|
||||
return ReversedType.getInstance(type);
|
||||
|
|
|
|||
|
|
@ -120,6 +120,13 @@ public final class DropFunctionStatement extends AlterSchemaStatement
|
|||
if (!dependentAggregates.isEmpty())
|
||||
throw ire("Function '%s' is still referenced by aggregates %s", name, dependentAggregates);
|
||||
|
||||
String dependentTables = keyspace.tablesUsingFunction(function)
|
||||
.map(table -> table.name)
|
||||
.collect(joining(", "));
|
||||
|
||||
if (!dependentTables.isEmpty())
|
||||
throw ire("Function '%s' is still referenced by column masks in tables %s", name, dependentTables);
|
||||
|
||||
return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.userFunctions.without(function)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ public class Columns extends AbstractCollection<ColumnMetadata> implements Colle
|
|||
ColumnIdentifier.getInterned(ByteBufferUtil.EMPTY_BYTE_BUFFER, UTF8Type.instance),
|
||||
SetType.getInstance(UTF8Type.instance, true),
|
||||
ColumnMetadata.NO_POSITION,
|
||||
ColumnMetadata.Kind.STATIC);
|
||||
ColumnMetadata.Kind.STATIC,
|
||||
null);
|
||||
|
||||
public static final ColumnMetadata FIRST_COMPLEX_REGULAR =
|
||||
new ColumnMetadata("",
|
||||
|
|
@ -69,7 +70,8 @@ public class Columns extends AbstractCollection<ColumnMetadata> implements Colle
|
|||
ColumnIdentifier.getInterned(ByteBufferUtil.EMPTY_BYTE_BUFFER, UTF8Type.instance),
|
||||
SetType.getInstance(UTF8Type.instance, true),
|
||||
ColumnMetadata.NO_POSITION,
|
||||
ColumnMetadata.Kind.REGULAR);
|
||||
ColumnMetadata.Kind.REGULAR,
|
||||
null);
|
||||
|
||||
private final Object[] columns;
|
||||
private final int complexIdx; // Index of the first complex column
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ public abstract class GroupMaker
|
|||
{
|
||||
super(comparator, clusteringPrefixSize, state);
|
||||
this.selector = selector;
|
||||
this.input = new Selector.InputRow(ProtocolVersion.CURRENT, columns);
|
||||
this.input = new Selector.InputRow(ProtocolVersion.CURRENT, columns, false);
|
||||
this.lastOutput = lastClustering == null ? null :
|
||||
executeSelector(lastClustering.bufferAt(clusteringPrefixSize - 1));
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ public abstract class GroupMaker
|
|||
{
|
||||
super(comparator, clusteringPrefixSize);
|
||||
this.selector = selector;
|
||||
this.input = new Selector.InputRow(ProtocolVersion.CURRENT, columns);
|
||||
this.input = new Selector.InputRow(ProtocolVersion.CURRENT, columns, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -21,11 +21,14 @@ import java.nio.ByteBuffer;
|
|||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.collect.Collections2;
|
||||
|
||||
import org.apache.cassandra.cql3.*;
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.cql3.selection.Selectable;
|
||||
import org.apache.cassandra.cql3.selection.Selector;
|
||||
import org.apache.cassandra.cql3.selection.SimpleSelector;
|
||||
|
|
@ -95,6 +98,12 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
*/
|
||||
private final long comparisonOrder;
|
||||
|
||||
/**
|
||||
* Masking function used to dynamically mask the contents of this column.
|
||||
*/
|
||||
@Nullable
|
||||
private final ColumnMask mask;
|
||||
|
||||
private static long comparisonOrder(Kind kind, boolean isComplex, long position, ColumnIdentifier name)
|
||||
{
|
||||
assert position >= 0 && position < 1 << 12;
|
||||
|
|
@ -106,52 +115,58 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
|
||||
public static ColumnMetadata partitionKeyColumn(TableMetadata table, ByteBuffer name, AbstractType<?> type, int position)
|
||||
{
|
||||
return new ColumnMetadata(table, name, type, position, Kind.PARTITION_KEY);
|
||||
return new ColumnMetadata(table, name, type, position, Kind.PARTITION_KEY, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata partitionKeyColumn(String keyspace, String table, String name, AbstractType<?> type, int position)
|
||||
{
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.PARTITION_KEY);
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.PARTITION_KEY, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata clusteringColumn(TableMetadata table, ByteBuffer name, AbstractType<?> type, int position)
|
||||
{
|
||||
return new ColumnMetadata(table, name, type, position, Kind.CLUSTERING);
|
||||
return new ColumnMetadata(table, name, type, position, Kind.CLUSTERING, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata clusteringColumn(String keyspace, String table, String name, AbstractType<?> type, int position)
|
||||
{
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.CLUSTERING);
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, position, Kind.CLUSTERING, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata regularColumn(TableMetadata table, ByteBuffer name, AbstractType<?> type)
|
||||
{
|
||||
return new ColumnMetadata(table, name, type, NO_POSITION, Kind.REGULAR);
|
||||
return new ColumnMetadata(table, name, type, NO_POSITION, Kind.REGULAR, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata regularColumn(String keyspace, String table, String name, AbstractType<?> type)
|
||||
{
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.REGULAR);
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.REGULAR, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata staticColumn(TableMetadata table, ByteBuffer name, AbstractType<?> type)
|
||||
{
|
||||
return new ColumnMetadata(table, name, type, NO_POSITION, Kind.STATIC);
|
||||
return new ColumnMetadata(table, name, type, NO_POSITION, Kind.STATIC, null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata staticColumn(String keyspace, String table, String name, AbstractType<?> type)
|
||||
{
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.STATIC);
|
||||
return new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, NO_POSITION, Kind.STATIC, null);
|
||||
}
|
||||
|
||||
public ColumnMetadata(TableMetadata table, ByteBuffer name, AbstractType<?> type, int position, Kind kind)
|
||||
public ColumnMetadata(TableMetadata table,
|
||||
ByteBuffer name,
|
||||
AbstractType<?> type,
|
||||
int position,
|
||||
Kind kind,
|
||||
@Nullable ColumnMask mask)
|
||||
{
|
||||
this(table.keyspace,
|
||||
table.name,
|
||||
ColumnIdentifier.getInterned(name, UTF8Type.instance),
|
||||
type,
|
||||
position,
|
||||
kind);
|
||||
kind,
|
||||
mask);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
@ -160,18 +175,26 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
ColumnIdentifier name,
|
||||
AbstractType<?> type,
|
||||
int position,
|
||||
Kind kind)
|
||||
Kind kind,
|
||||
@Nullable ColumnMask mask)
|
||||
{
|
||||
super(ksName, cfName, name, type);
|
||||
assert name != null && type != null && kind != null;
|
||||
assert (position == NO_POSITION) == !kind.isPrimaryKeyKind(); // The position really only make sense for partition and clustering columns (and those must have one),
|
||||
// so make sure we don't sneak it for something else since it'd breaks equals()
|
||||
|
||||
// The propagation of system distributed keyspaces at startup can be problematic for old nodes without DDM,
|
||||
// since those won't know what to do with the mask mutations. Thus, we don't support DDM on those keyspaces.
|
||||
if (mask != null && SchemaConstants.isReplicatedSystemKeyspace(ksName))
|
||||
throw new AssertionError("DDM is not supported on system distributed keyspaces");
|
||||
|
||||
this.kind = kind;
|
||||
this.position = position;
|
||||
this.cellPathComparator = makeCellPathComparator(kind, type);
|
||||
this.cellComparator = cellPathComparator == null ? ColumnData.comparator : (a, b) -> cellPathComparator.compare(a.path(), b.path());
|
||||
this.asymmetricCellPathComparator = cellPathComparator == null ? null : (a, b) -> cellPathComparator.compare(((Cell<?>)a).path(), (CellPath) b);
|
||||
this.comparisonOrder = comparisonOrder(kind, isComplex(), Math.max(0, position), name);
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
private static Comparator<CellPath> makeCellPathComparator(Kind kind, AbstractType<?> type)
|
||||
|
|
@ -203,17 +226,22 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
|
||||
public ColumnMetadata copy()
|
||||
{
|
||||
return new ColumnMetadata(ksName, cfName, name, type, position, kind);
|
||||
return new ColumnMetadata(ksName, cfName, name, type, position, kind, mask);
|
||||
}
|
||||
|
||||
public ColumnMetadata withNewName(ColumnIdentifier newName)
|
||||
{
|
||||
return new ColumnMetadata(ksName, cfName, newName, type, position, kind);
|
||||
return new ColumnMetadata(ksName, cfName, newName, type, position, kind, mask);
|
||||
}
|
||||
|
||||
public ColumnMetadata withNewType(AbstractType<?> newType)
|
||||
{
|
||||
return new ColumnMetadata(ksName, cfName, name, newType, position, kind);
|
||||
return new ColumnMetadata(ksName, cfName, name, newType, position, kind, mask);
|
||||
}
|
||||
|
||||
public ColumnMetadata withNewMask(@Nullable ColumnMask newMask)
|
||||
{
|
||||
return new ColumnMetadata(ksName, cfName, name, type, position, kind, newMask);
|
||||
}
|
||||
|
||||
public boolean isPartitionKey()
|
||||
|
|
@ -231,6 +259,11 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
return kind == Kind.STATIC;
|
||||
}
|
||||
|
||||
public boolean isMasked()
|
||||
{
|
||||
return mask != null;
|
||||
}
|
||||
|
||||
public boolean isRegular()
|
||||
{
|
||||
return kind == Kind.REGULAR;
|
||||
|
|
@ -249,6 +282,12 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
return position;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ColumnMask getMask()
|
||||
{
|
||||
return mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
|
|
@ -269,7 +308,8 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
&& kind == other.kind
|
||||
&& position == other.position
|
||||
&& ksName.equals(other.ksName)
|
||||
&& cfName.equals(other.cfName);
|
||||
&& cfName.equals(other.cfName)
|
||||
&& Objects.equals(mask, other.mask);
|
||||
}
|
||||
|
||||
Optional<Difference> compare(ColumnMetadata other)
|
||||
|
|
@ -299,6 +339,7 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
result = 31 * result + (type == null ? 0 : type.hashCode());
|
||||
result = 31 * result + (kind == null ? 0 : kind.hashCode());
|
||||
result = 31 * result + position;
|
||||
result = 31 * result + (mask == null ? 0 : mask.hashCode());
|
||||
hash = result;
|
||||
}
|
||||
return result;
|
||||
|
|
@ -334,7 +375,7 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
@Override
|
||||
public boolean processesSelection()
|
||||
{
|
||||
return false;
|
||||
return isMasked();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -433,6 +474,9 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
|
||||
if (isStatic())
|
||||
builder.append(" static");
|
||||
|
||||
if (isMasked())
|
||||
mask.appendCqlTo(builder);
|
||||
}
|
||||
|
||||
public static String toCQLString(Iterable<ColumnMetadata> defs)
|
||||
|
|
@ -488,7 +532,7 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
|
|||
|
||||
public Selector.Factory newSelectorFactory(TableMetadata table, AbstractType<?> expectedType, List<ColumnMetadata> defs, VariableSpecifications boundNames) throws InvalidRequestException
|
||||
{
|
||||
return SimpleSelector.newFactory(this, addAndGetIndex(this, defs));
|
||||
return SimpleSelector.newFactory(this, addAndGetIndex(this, defs), false);
|
||||
}
|
||||
|
||||
public AbstractType<?> getExactTypeIfKnown(String keyspace)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package org.apache.cassandra.schema;
|
|||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ import com.google.common.collect.Iterables;
|
|||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CqlBuilder;
|
||||
import org.apache.cassandra.cql3.SchemaElement;
|
||||
import org.apache.cassandra.cql3.functions.Function;
|
||||
import org.apache.cassandra.cql3.functions.UDAggregate;
|
||||
import org.apache.cassandra.cql3.functions.UDFunction;
|
||||
import org.apache.cassandra.db.marshal.UserType;
|
||||
|
|
@ -173,6 +175,15 @@ public final class KeyspaceMetadata implements SchemaElement
|
|||
return any(tables, t -> t.indexes.has(indexName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param function a user function
|
||||
* @return a stream of tables within this keyspace that have column masks using the specified user function
|
||||
*/
|
||||
public Stream<TableMetadata> tablesUsingFunction(Function function)
|
||||
{
|
||||
return tables.stream().filter(table -> table.dependsOn(function));
|
||||
}
|
||||
|
||||
public String findAvailableIndexName(String baseName)
|
||||
{
|
||||
if (!hasIndex(baseName))
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import org.antlr.runtime.RecognitionException;
|
|||
import org.apache.cassandra.config.*;
|
||||
import org.apache.cassandra.cql3.*;
|
||||
import org.apache.cassandra.cql3.functions.*;
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.cql3.statements.schema.CreateTableStatement;
|
||||
import org.apache.cassandra.db.*;
|
||||
import org.apache.cassandra.db.filter.ColumnFilter;
|
||||
|
|
@ -113,7 +114,7 @@ public final class SchemaKeyspace
|
|||
+ "extensions frozen<map<text, blob>>,"
|
||||
+ "flags frozen<set<text>>," // SUPER, COUNTER, DENSE, COMPOUND
|
||||
+ "gc_grace_seconds int,"
|
||||
+ "incremental_backups boolean,"
|
||||
+ "incremental_backups boolean,"
|
||||
+ "id uuid,"
|
||||
+ "max_index_interval int,"
|
||||
+ "memtable_flush_period_in_ms int,"
|
||||
|
|
@ -139,6 +140,20 @@ public final class SchemaKeyspace
|
|||
+ "type text,"
|
||||
+ "PRIMARY KEY ((keyspace_name), table_name, column_name))");
|
||||
|
||||
private static final TableMetadata ColumnMasks =
|
||||
parse(COLUMN_MASKS,
|
||||
"column dynamic data masks",
|
||||
"CREATE TABLE %s ("
|
||||
+ "keyspace_name text,"
|
||||
+ "table_name text,"
|
||||
+ "column_name text,"
|
||||
+ "function_keyspace text,"
|
||||
+ "function_name text,"
|
||||
+ "function_argument_types frozen<list<text>>,"
|
||||
+ "function_argument_values frozen<list<text>>,"
|
||||
+ "function_argument_nulls frozen<list<boolean>>," // arguments that are null
|
||||
+ "PRIMARY KEY ((keyspace_name), table_name, column_name))");
|
||||
|
||||
private static final TableMetadata DroppedColumns =
|
||||
parse(DROPPED_COLUMNS,
|
||||
"dropped column registry",
|
||||
|
|
@ -182,7 +197,7 @@ public final class SchemaKeyspace
|
|||
+ "default_time_to_live int,"
|
||||
+ "extensions frozen<map<text, blob>>,"
|
||||
+ "gc_grace_seconds int,"
|
||||
+ "incremental_backups boolean,"
|
||||
+ "incremental_backups boolean,"
|
||||
+ "id uuid,"
|
||||
+ "include_all_columns boolean,"
|
||||
+ "max_index_interval int,"
|
||||
|
|
@ -244,8 +259,17 @@ public final class SchemaKeyspace
|
|||
+ "state_type text,"
|
||||
+ "PRIMARY KEY ((keyspace_name), aggregate_name, argument_types))");
|
||||
|
||||
private static final List<TableMetadata> ALL_TABLE_METADATA =
|
||||
ImmutableList.of(Keyspaces, Tables, Columns, Triggers, DroppedColumns, Views, Types, Functions, Aggregates, Indexes);
|
||||
private static final List<TableMetadata> ALL_TABLE_METADATA = ImmutableList.of(Keyspaces,
|
||||
Tables,
|
||||
Columns,
|
||||
ColumnMasks,
|
||||
Triggers,
|
||||
DroppedColumns,
|
||||
Views,
|
||||
Types,
|
||||
Functions,
|
||||
Aggregates,
|
||||
Indexes);
|
||||
|
||||
private static TableMetadata parse(String name, String description, String cql)
|
||||
{
|
||||
|
|
@ -688,7 +712,7 @@ public final class SchemaKeyspace
|
|||
{
|
||||
AbstractType<?> type = column.type;
|
||||
if (type instanceof ReversedType)
|
||||
type = ((ReversedType) type).baseType;
|
||||
type = ((ReversedType<?>) type).baseType;
|
||||
|
||||
builder.update(Columns)
|
||||
.row(table.name, column.name.toString())
|
||||
|
|
@ -697,6 +721,52 @@ public final class SchemaKeyspace
|
|||
.add("position", column.position())
|
||||
.add("clustering_order", column.clusteringOrder().toString().toLowerCase())
|
||||
.add("type", type.asCQL3Type().toString());
|
||||
|
||||
ColumnMask mask = column.getMask();
|
||||
if (SchemaConstants.isReplicatedSystemKeyspace(table.keyspace))
|
||||
{
|
||||
// The propagation of system distributed keyspaces at startup can be problematic for old nodes without DDM,
|
||||
// since those won't know what to do with the mask mutations. Thus, we don't support DDM on those keyspaces.
|
||||
assert mask == null : "Dynamic data masking shouldn't be used on system distributed keyspaces";
|
||||
}
|
||||
else
|
||||
{
|
||||
Row.SimpleBuilder maskBuilder = builder.update(ColumnMasks).row(table.name, column.name.toString());
|
||||
|
||||
if (mask == null)
|
||||
{
|
||||
maskBuilder.delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
FunctionName maskFunctionName = mask.function.name();
|
||||
|
||||
// Some arguments of the masking function can be null, but the CQL's list type that stores them doesn't
|
||||
// accept nulls, so we use a parallel list of booleans to store what arguments are null.
|
||||
List<AbstractType<?>> partialTypes = mask.partialArgumentTypes();
|
||||
List<ByteBuffer> partialValues = mask.partialArgumentValues();
|
||||
int numArgs = partialTypes.size();
|
||||
List<String> types = new ArrayList<>(numArgs);
|
||||
List<String> values = new ArrayList<>(numArgs);
|
||||
List<Boolean> nulls = new ArrayList<>(numArgs);
|
||||
for (int i = 0; i < numArgs; i++)
|
||||
{
|
||||
AbstractType<?> argType = partialTypes.get(i);
|
||||
types.add(argType.asCQL3Type().toString());
|
||||
|
||||
ByteBuffer argValue = partialValues.get(i);
|
||||
boolean isNull = argValue == null;
|
||||
nulls.add(isNull);
|
||||
values.add(isNull ? "" : argType.getString(argValue));
|
||||
}
|
||||
|
||||
maskBuilder.add("function_keyspace", maskFunctionName.keyspace)
|
||||
.add("function_name", maskFunctionName.name)
|
||||
.add("function_argument_types", types)
|
||||
.add("function_argument_values", values)
|
||||
.add("function_argument_nulls", nulls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void dropColumnFromSchemaMutation(TableMetadata table, ColumnMetadata column, Mutation.SimpleBuilder builder)
|
||||
|
|
@ -876,9 +946,9 @@ public final class SchemaKeyspace
|
|||
{
|
||||
KeyspaceParams params = fetchKeyspaceParams(keyspaceName);
|
||||
Types types = fetchTypes(keyspaceName);
|
||||
Tables tables = fetchTables(keyspaceName, types);
|
||||
Views views = fetchViews(keyspaceName, types);
|
||||
UserFunctions functions = fetchFunctions(keyspaceName, types);
|
||||
Tables tables = fetchTables(keyspaceName, types, functions);
|
||||
Views views = fetchViews(keyspaceName, types, functions);
|
||||
return KeyspaceMetadata.create(keyspaceName, params, tables, views, types, functions);
|
||||
}
|
||||
|
||||
|
|
@ -907,7 +977,7 @@ public final class SchemaKeyspace
|
|||
return types.build();
|
||||
}
|
||||
|
||||
private static Tables fetchTables(String keyspaceName, Types types)
|
||||
private static Tables fetchTables(String keyspaceName, Types types, UserFunctions functions)
|
||||
{
|
||||
String query = format("SELECT table_name FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, TABLES);
|
||||
|
||||
|
|
@ -917,7 +987,7 @@ public final class SchemaKeyspace
|
|||
String tableName = row.getString("table_name");
|
||||
try
|
||||
{
|
||||
tables.add(fetchTable(keyspaceName, tableName, types));
|
||||
tables.add(fetchTable(keyspaceName, tableName, types, functions));
|
||||
}
|
||||
catch (MissingColumns exc)
|
||||
{
|
||||
|
|
@ -946,7 +1016,7 @@ public final class SchemaKeyspace
|
|||
return tables.build();
|
||||
}
|
||||
|
||||
private static TableMetadata fetchTable(String keyspaceName, String tableName, Types types)
|
||||
private static TableMetadata fetchTable(String keyspaceName, String tableName, Types types, UserFunctions functions)
|
||||
{
|
||||
String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, TABLES);
|
||||
UntypedResultSet rows = query(query, keyspaceName, tableName);
|
||||
|
|
@ -958,7 +1028,7 @@ public final class SchemaKeyspace
|
|||
return TableMetadata.builder(keyspaceName, tableName, TableId.fromUUID(row.getUUID("id")))
|
||||
.flags(flags)
|
||||
.params(createTableParamsFromRow(row))
|
||||
.addColumns(fetchColumns(keyspaceName, tableName, types))
|
||||
.addColumns(fetchColumns(keyspaceName, tableName, types, functions))
|
||||
.droppedColumns(fetchDroppedColumns(keyspaceName, tableName))
|
||||
.indexes(fetchIndexes(keyspaceName, tableName))
|
||||
.triggers(fetchTriggers(keyspaceName, tableName))
|
||||
|
|
@ -1002,7 +1072,7 @@ public final class SchemaKeyspace
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
private static List<ColumnMetadata> fetchColumns(String keyspace, String table, Types types)
|
||||
private static List<ColumnMetadata> fetchColumns(String keyspace, String table, Types types, UserFunctions functions)
|
||||
{
|
||||
String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, COLUMNS);
|
||||
UntypedResultSet columnRows = query(query, keyspace, table);
|
||||
|
|
@ -1010,7 +1080,7 @@ public final class SchemaKeyspace
|
|||
throw new MissingColumns("Columns not found in schema table for " + keyspace + '.' + table);
|
||||
|
||||
List<ColumnMetadata> columns = new ArrayList<>();
|
||||
columnRows.forEach(row -> columns.add(createColumnFromRow(row, types)));
|
||||
columnRows.forEach(row -> columns.add(createColumnFromRow(row, types, functions)));
|
||||
|
||||
if (columns.stream().noneMatch(ColumnMetadata::isPartitionKey))
|
||||
throw new MissingColumns("No partition key columns found in schema table for " + keyspace + "." + table);
|
||||
|
|
@ -1019,7 +1089,7 @@ public final class SchemaKeyspace
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static ColumnMetadata createColumnFromRow(UntypedResultSet.Row row, Types types)
|
||||
public static ColumnMetadata createColumnFromRow(UntypedResultSet.Row row, Types types, UserFunctions functions)
|
||||
{
|
||||
String keyspace = row.getString("keyspace_name");
|
||||
String table = row.getString("table_name");
|
||||
|
|
@ -1035,7 +1105,53 @@ public final class SchemaKeyspace
|
|||
|
||||
ColumnIdentifier name = new ColumnIdentifier(row.getBytes("column_name_bytes"), row.getString("column_name"));
|
||||
|
||||
return new ColumnMetadata(keyspace, table, name, type, position, kind);
|
||||
ColumnMask mask = null;
|
||||
String query = format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ? AND column_name = ?",
|
||||
SchemaConstants.SCHEMA_KEYSPACE_NAME, COLUMN_MASKS);
|
||||
UntypedResultSet columnMasks = query(query, keyspace, table, name.toString());
|
||||
if (!columnMasks.isEmpty())
|
||||
{
|
||||
UntypedResultSet.Row maskRow = columnMasks.one();
|
||||
FunctionName functionName = new FunctionName(maskRow.getString("function_keyspace"), maskRow.getString("function_name"));
|
||||
|
||||
List<String> partialArgumentTypes = maskRow.getFrozenList("function_argument_types", UTF8Type.instance);
|
||||
List<AbstractType<?>> argumentTypes = new ArrayList<>(1 + partialArgumentTypes.size());
|
||||
argumentTypes.add(type);
|
||||
for (String argumentType : partialArgumentTypes)
|
||||
{
|
||||
argumentTypes.add(CQLTypeParser.parse(keyspace, argumentType, types));
|
||||
}
|
||||
|
||||
Function function = FunctionResolver.get(keyspace, functionName, argumentTypes, null, null, null, functions);
|
||||
if (function == null)
|
||||
{
|
||||
throw new AssertionError(format("Unable to find masking function %s(%s) for column %s.%s.%s",
|
||||
functionName, argumentTypes, keyspace, table, name));
|
||||
}
|
||||
else if (!(function instanceof ScalarFunction))
|
||||
{
|
||||
throw new AssertionError(format("Column %s.%s.%s is unexpectedly masked with function %s " +
|
||||
"which is not a scalar masking function",
|
||||
keyspace, table, name, function));
|
||||
}
|
||||
|
||||
// Some arguments of the masking function can be null, but the CQL's list type that stores them doesn't
|
||||
// accept nulls, so we use a parallel list of booleans to store what arguments are null.
|
||||
List<Boolean> nulls = maskRow.getFrozenList("function_argument_nulls", BooleanType.instance);
|
||||
List<String> valuesAsCQL = maskRow.getFrozenList("function_argument_values", UTF8Type.instance);
|
||||
ByteBuffer[] values = new ByteBuffer[valuesAsCQL.size()];
|
||||
for (int i = 0; i < valuesAsCQL.size(); i++)
|
||||
{
|
||||
if (nulls.get(i))
|
||||
values[i] = null;
|
||||
else
|
||||
values[i] = argumentTypes.get(i + 1).fromString(valuesAsCQL.get(i));
|
||||
}
|
||||
|
||||
mask = ColumnMask.build((ScalarFunction) function, values);
|
||||
}
|
||||
|
||||
return new ColumnMetadata(keyspace, table, name, type, position, kind, mask);
|
||||
}
|
||||
|
||||
private static Map<ByteBuffer, DroppedColumn> fetchDroppedColumns(String keyspace, String table)
|
||||
|
|
@ -1067,7 +1183,7 @@ public final class SchemaKeyspace
|
|||
assert kind == ColumnMetadata.Kind.REGULAR || kind == ColumnMetadata.Kind.STATIC
|
||||
: "Unexpected dropped column kind: " + kind;
|
||||
|
||||
ColumnMetadata column = new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, ColumnMetadata.NO_POSITION, kind);
|
||||
ColumnMetadata column = new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, ColumnMetadata.NO_POSITION, kind, null);
|
||||
long droppedTime = TimeUnit.MILLISECONDS.toMicros(row.getLong("dropped_time"));
|
||||
return new DroppedColumn(column, droppedTime);
|
||||
}
|
||||
|
|
@ -1103,17 +1219,17 @@ public final class SchemaKeyspace
|
|||
return new TriggerMetadata(name, classOption);
|
||||
}
|
||||
|
||||
private static Views fetchViews(String keyspaceName, Types types)
|
||||
private static Views fetchViews(String keyspaceName, Types types, UserFunctions functions)
|
||||
{
|
||||
String query = format("SELECT view_name FROM %s.%s WHERE keyspace_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, VIEWS);
|
||||
|
||||
Views.Builder views = org.apache.cassandra.schema.Views.builder();
|
||||
for (UntypedResultSet.Row row : query(query, keyspaceName))
|
||||
views.put(fetchView(keyspaceName, row.getString("view_name"), types));
|
||||
views.put(fetchView(keyspaceName, row.getString("view_name"), types, functions));
|
||||
return views.build();
|
||||
}
|
||||
|
||||
private static ViewMetadata fetchView(String keyspaceName, String viewName, Types types)
|
||||
private static ViewMetadata fetchView(String keyspaceName, String viewName, Types types, UserFunctions functions)
|
||||
{
|
||||
String query = String.format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND view_name = ?", SchemaConstants.SCHEMA_KEYSPACE_NAME, VIEWS);
|
||||
UntypedResultSet rows = query(query, keyspaceName, viewName);
|
||||
|
|
@ -1126,7 +1242,7 @@ public final class SchemaKeyspace
|
|||
boolean includeAll = row.getBoolean("include_all_columns");
|
||||
String whereClauseString = row.getString("where_clause");
|
||||
|
||||
List<ColumnMetadata> columns = fetchColumns(keyspaceName, viewName, types);
|
||||
List<ColumnMetadata> columns = fetchColumns(keyspaceName, viewName, types, functions);
|
||||
|
||||
TableMetadata metadata =
|
||||
TableMetadata.builder(keyspaceName, viewName, TableId.fromUUID(row.getUUID("id")))
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public class SchemaKeyspaceTables
|
|||
public static final String KEYSPACES = "keyspaces";
|
||||
public static final String TABLES = "tables";
|
||||
public static final String COLUMNS = "columns";
|
||||
public static final String COLUMN_MASKS = "column_masks";
|
||||
public static final String DROPPED_COLUMNS = "dropped_columns";
|
||||
public static final String TRIGGERS = "triggers";
|
||||
public static final String VIEWS = "views";
|
||||
|
|
@ -45,7 +46,8 @@ public class SchemaKeyspaceTables
|
|||
*
|
||||
* See CASSANDRA-12213 for more details.
|
||||
*/
|
||||
public static final ImmutableList<String> ALL = ImmutableList.of(COLUMNS,
|
||||
public static final ImmutableList<String> ALL = ImmutableList.of(COLUMN_MASKS,
|
||||
COLUMNS,
|
||||
DROPPED_COLUMNS,
|
||||
TRIGGERS,
|
||||
TYPES,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ import org.apache.cassandra.config.DatabaseDescriptor;
|
|||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.CqlBuilder;
|
||||
import org.apache.cassandra.cql3.SchemaElement;
|
||||
import org.apache.cassandra.cql3.functions.Function;
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.db.Clustering;
|
||||
import org.apache.cassandra.db.ClusteringComparator;
|
||||
import org.apache.cassandra.db.Columns;
|
||||
|
|
@ -442,6 +444,35 @@ public class TableMetadata implements SchemaElement
|
|||
return !staticColumns().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the table has any masked column, {@code false} otherwise.
|
||||
*/
|
||||
public boolean hasMaskedColumns()
|
||||
{
|
||||
for (ColumnMetadata column : columns.values())
|
||||
{
|
||||
if (column.isMasked())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param function a user function
|
||||
* @return {@code true} if the table has any masked column depending on the specified user function,
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
public boolean dependsOn(Function function)
|
||||
{
|
||||
for (ColumnMetadata column : columns.values())
|
||||
{
|
||||
ColumnMask mask = column.getMask();
|
||||
if (mask != null && mask.function.name().equals(function.name()))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void validate()
|
||||
{
|
||||
if (!isNameValid(keyspace))
|
||||
|
|
@ -931,44 +962,84 @@ public class TableMetadata implements SchemaElement
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder addPartitionKeyColumn(String name, AbstractType type)
|
||||
public Builder addPartitionKeyColumn(String name, AbstractType<?> type)
|
||||
{
|
||||
return addPartitionKeyColumn(ColumnIdentifier.getInterned(name, false), type);
|
||||
return addPartitionKeyColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addPartitionKeyColumn(ColumnIdentifier name, AbstractType type)
|
||||
public Builder addPartitionKeyColumn(String name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, partitionKeyColumns.size(), ColumnMetadata.Kind.PARTITION_KEY));
|
||||
return addPartitionKeyColumn(ColumnIdentifier.getInterned(name, false), type, mask);
|
||||
}
|
||||
|
||||
public Builder addClusteringColumn(String name, AbstractType type)
|
||||
public Builder addPartitionKeyColumn(ColumnIdentifier name, AbstractType<?> type)
|
||||
{
|
||||
return addClusteringColumn(ColumnIdentifier.getInterned(name, false), type);
|
||||
return addPartitionKeyColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addClusteringColumn(ColumnIdentifier name, AbstractType type)
|
||||
public Builder addPartitionKeyColumn(ColumnIdentifier name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, clusteringColumns.size(), ColumnMetadata.Kind.CLUSTERING));
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, partitionKeyColumns.size(), ColumnMetadata.Kind.PARTITION_KEY, mask));
|
||||
}
|
||||
|
||||
public Builder addRegularColumn(String name, AbstractType type)
|
||||
public Builder addClusteringColumn(String name, AbstractType<?> type)
|
||||
{
|
||||
return addRegularColumn(ColumnIdentifier.getInterned(name, false), type);
|
||||
return addClusteringColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addRegularColumn(ColumnIdentifier name, AbstractType type)
|
||||
public Builder addClusteringColumn(String name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.REGULAR));
|
||||
return addClusteringColumn(ColumnIdentifier.getInterned(name, false), type, mask);
|
||||
}
|
||||
|
||||
public Builder addStaticColumn(String name, AbstractType type)
|
||||
public Builder addClusteringColumn(ColumnIdentifier name, AbstractType<?> type)
|
||||
{
|
||||
return addStaticColumn(ColumnIdentifier.getInterned(name, false), type);
|
||||
return addClusteringColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addStaticColumn(ColumnIdentifier name, AbstractType type)
|
||||
public Builder addClusteringColumn(ColumnIdentifier name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.STATIC));
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, clusteringColumns.size(), ColumnMetadata.Kind.CLUSTERING, mask));
|
||||
}
|
||||
|
||||
public Builder addRegularColumn(String name, AbstractType<?> type)
|
||||
{
|
||||
return addRegularColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addRegularColumn(String name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addRegularColumn(ColumnIdentifier.getInterned(name, false), type, mask);
|
||||
}
|
||||
|
||||
public Builder addRegularColumn(ColumnIdentifier name, AbstractType<?> type)
|
||||
{
|
||||
return addRegularColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addRegularColumn(ColumnIdentifier name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.REGULAR, mask));
|
||||
}
|
||||
|
||||
public Builder addStaticColumn(String name, AbstractType<?> type)
|
||||
{
|
||||
return addStaticColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addStaticColumn(String name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addStaticColumn(ColumnIdentifier.getInterned(name, false), type, mask);
|
||||
}
|
||||
|
||||
public Builder addStaticColumn(ColumnIdentifier name, AbstractType<?> type)
|
||||
{
|
||||
return addStaticColumn(name, type, null);
|
||||
}
|
||||
|
||||
public Builder addStaticColumn(ColumnIdentifier name, AbstractType<?> type, @Nullable ColumnMask mask)
|
||||
{
|
||||
return addColumn(new ColumnMetadata(keyspace, this.name, name, type, ColumnMetadata.NO_POSITION, ColumnMetadata.Kind.STATIC, mask));
|
||||
}
|
||||
|
||||
public Builder addColumn(ColumnMetadata column)
|
||||
|
|
@ -1093,6 +1164,19 @@ public class TableMetadata implements SchemaElement
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder alterColumnMask(ColumnIdentifier name, @Nullable ColumnMask mask)
|
||||
{
|
||||
ColumnMetadata column = columns.get(name.bytes);
|
||||
if (column == null)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
ColumnMetadata newColumn = column.withNewMask(mask);
|
||||
|
||||
updateColumn(column, newColumn);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder alterColumnType(ColumnIdentifier name, AbstractType<?> type)
|
||||
{
|
||||
ColumnMetadata column = columns.get(name.bytes);
|
||||
|
|
@ -1101,6 +1185,13 @@ public class TableMetadata implements SchemaElement
|
|||
|
||||
ColumnMetadata newColumn = column.withNewType(type);
|
||||
|
||||
updateColumn(column, newColumn);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void updateColumn(ColumnMetadata column, ColumnMetadata newColumn)
|
||||
{
|
||||
switch (column.kind)
|
||||
{
|
||||
case PARTITION_KEY:
|
||||
|
|
@ -1117,8 +1208,6 @@ public class TableMetadata implements SchemaElement
|
|||
}
|
||||
|
||||
columns.put(column.name.bytes, newColumn);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1543,7 +1632,7 @@ public class TableMetadata implements SchemaElement
|
|||
for (ColumnMetadata c : regularAndStaticColumns)
|
||||
{
|
||||
if (c.isStatic())
|
||||
columns.add(new ColumnMetadata(c.ksName, c.cfName, c.name, c.type, -1, ColumnMetadata.Kind.REGULAR));
|
||||
columns.add(new ColumnMetadata(c.ksName, c.cfName, c.name, c.type, -1, ColumnMetadata.Kind.REGULAR, c.getMask()));
|
||||
}
|
||||
otherColumns = columns.iterator();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@ package org.apache.cassandra.schema;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
import org.apache.cassandra.cql3.*;
|
||||
import org.apache.cassandra.cql3.functions.masking.ColumnMask;
|
||||
import org.apache.cassandra.db.marshal.UserType;
|
||||
|
||||
public final class ViewMetadata implements SchemaElement
|
||||
|
|
@ -158,6 +161,15 @@ public final class ViewMetadata implements SchemaElement
|
|||
metadata.unbuild().addColumn(column).build());
|
||||
}
|
||||
|
||||
public ViewMetadata withNewColumnMask(ColumnIdentifier name, @Nullable ColumnMask mask)
|
||||
{
|
||||
return new ViewMetadata(baseTableId,
|
||||
baseTableName,
|
||||
includeAllColumns,
|
||||
whereClause,
|
||||
metadata.unbuild().alterColumnMask(name, mask).build());
|
||||
}
|
||||
|
||||
public void appendCqlTo(CqlBuilder builder,
|
||||
boolean internals,
|
||||
boolean ifNotExists)
|
||||
|
|
|
|||
|
|
@ -414,6 +414,27 @@ public class ClientState
|
|||
ensurePermission(table.keyspace, perm, table.resource);
|
||||
}
|
||||
|
||||
public boolean hasTablePermission(TableMetadata table, Permission perm)
|
||||
{
|
||||
if (isInternal)
|
||||
return true;
|
||||
|
||||
validateLogin();
|
||||
|
||||
if (!DatabaseDescriptor.getAuthorizer().requireAuthorization())
|
||||
return true;
|
||||
|
||||
List<? extends IResource> resources = Resources.chain(table.resource);
|
||||
if (DatabaseDescriptor.getAuthFromRoot())
|
||||
resources = Lists.reverse(resources);
|
||||
|
||||
for (IResource r : resources)
|
||||
if (authorize(r).contains(perm))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ensurePermission(String keyspace, Permission perm, DataResource resource)
|
||||
{
|
||||
validateKeyspace(keyspace);
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ public class NativeSSTableLoaderClient extends SSTableLoader.Client
|
|||
|
||||
int position = row.getInt("position");
|
||||
org.apache.cassandra.schema.ColumnMetadata.Kind kind = ColumnMetadata.Kind.valueOf(row.getString("kind").toUpperCase());
|
||||
return new ColumnMetadata(keyspace, table, name, type, position, kind);
|
||||
return new ColumnMetadata(keyspace, table, name, type, position, kind, null);
|
||||
}
|
||||
|
||||
private static DroppedColumn createDroppedColumnFromRow(Row row, String keyspace, String table)
|
||||
|
|
@ -220,7 +220,7 @@ public class NativeSSTableLoaderClient extends SSTableLoader.Client
|
|||
String name = row.getString("column_name");
|
||||
AbstractType<?> type = CQLTypeParser.parse(keyspace, row.getString("type"), Types.none());
|
||||
ColumnMetadata.Kind kind = ColumnMetadata.Kind.valueOf(row.getString("kind").toUpperCase());
|
||||
ColumnMetadata column = new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, ColumnMetadata.NO_POSITION, kind);
|
||||
ColumnMetadata column = new ColumnMetadata(keyspace, table, ColumnIdentifier.getInterned(name, true), type, ColumnMetadata.NO_POSITION, kind, null);
|
||||
long droppedTime = row.getTimestamp("dropped_time").getTime();
|
||||
return new DroppedColumn(column, droppedTime);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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.distributed.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.datastax.driver.core.ConsistencyLevel;
|
||||
import com.datastax.driver.core.ResultSet;
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.policies.LoadBalancingPolicy;
|
||||
import org.apache.cassandra.auth.CassandraRoleManager;
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.distributed.api.IInvokableInstance;
|
||||
import org.apache.cassandra.distributed.impl.RowUtil;
|
||||
import org.apache.cassandra.distributed.util.SingleHostLoadBalancingPolicy;
|
||||
|
||||
import static com.datastax.driver.core.Cluster.Builder;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.apache.cassandra.auth.CassandraRoleManager.DEFAULT_SUPERUSER_NAME;
|
||||
import static org.apache.cassandra.auth.CassandraRoleManager.DEFAULT_SUPERUSER_PASSWORD;
|
||||
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
||||
/**
|
||||
* Tests for dynamic data masking.
|
||||
*/
|
||||
public class ColumnMaskTest extends TestBaseImpl
|
||||
{
|
||||
private static final String SELECT = withKeyspace("SELECT * FROM %s.t");
|
||||
private static final String USERNAME = "ddm_user";
|
||||
private static final String PASSWORD = "ddm_password";
|
||||
|
||||
/**
|
||||
* Tests that column masks are propagated to all nodes in the cluster.
|
||||
*/
|
||||
@Test
|
||||
public void testMaskPropagation() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = createClusterWithAuhentication(3))
|
||||
{
|
||||
cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (k int PRIMARY KEY, v text MASKED WITH DEFAULT) WITH read_repair='NONE'"));
|
||||
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.t(k, v) VALUES (1, 'secret1')"));
|
||||
cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.t(k, v) VALUES (2, 'secret2')"));
|
||||
cluster.get(3).executeInternal(withKeyspace("INSERT INTO %s.t(k, v) VALUES (3, 'secret3')"));
|
||||
|
||||
assertRowsInAllCoordinators(cluster, row(1, "****"), row(2, "****"), row(3, "****"));
|
||||
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER v DROP MASKED"));
|
||||
assertRowsInAllCoordinators(cluster, row(1, "secret1"), row(2, "secret2"), row(3, "secret3"));
|
||||
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER v MASKED WITH mask_inner(null, 1)"));
|
||||
assertRowsInAllCoordinators(cluster, row(1, "******1"), row(2, "******2"), row(3, "******3"));
|
||||
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER v MASKED WITH mask_inner(3, null)"));
|
||||
assertRowsInAllCoordinators(cluster, row(1, "sec****"), row(2, "sec****"), row(3, "sec****"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that column masks are properly loaded at startup.
|
||||
*/
|
||||
@Test
|
||||
public void testMaskLoading() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = createClusterWithAuhentication(1))
|
||||
{
|
||||
IInvokableInstance node = cluster.get(1);
|
||||
|
||||
cluster.schemaChange(withKeyspace("CREATE FUNCTION %s.custom_mask(column text, replacement text) " +
|
||||
"RETURNS NULL ON NULL INPUT " +
|
||||
"RETURNS text " +
|
||||
"LANGUAGE java " +
|
||||
"AS 'return replacement;'"));
|
||||
cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (" +
|
||||
"a text MASKED WITH DEFAULT, " +
|
||||
"b text MASKED WITH mask_replace('redacted'), " +
|
||||
"c text MASKED WITH mask_inner(null, 1), " +
|
||||
"d text MASKED WITH mask_inner(3, null), " +
|
||||
"e text MASKED WITH %<s.custom_mask('obscured'), " +
|
||||
"PRIMARY KEY (a, b))"));
|
||||
String insert = withKeyspace("INSERT INTO %s.t(a, b, c, d, e) VALUES (?, ?, ?, ?, ?)");
|
||||
node.executeInternal(insert, "secret1", "secret1", "secret1", "secret1", "secret1");
|
||||
node.executeInternal(insert, "secret2", "secret2", "secret2", "secret2", "secret2");
|
||||
assertRowsWithRestart(node,
|
||||
row("****", "redacted", "******1", "sec****", "obscured"),
|
||||
row("****", "redacted", "******2", "sec****", "obscured"));
|
||||
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER a DROP MASKED"));
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER b MASKED WITH mask_null()"));
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER c MASKED WITH mask_inner(null, null, '#')"));
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER d MASKED WITH mask_inner(3, 1, '#')"));
|
||||
cluster.schemaChange(withKeyspace("ALTER TABLE %s.t ALTER e MASKED WITH %<s.custom_mask('censored')"));
|
||||
assertRowsWithRestart(node,
|
||||
row("secret1", null, "#######", "sec###1", "censored"),
|
||||
row("secret2", null, "#######", "sec###2", "censored"));
|
||||
}
|
||||
}
|
||||
|
||||
private static Cluster createClusterWithAuhentication(int nodeCount) throws IOException
|
||||
{
|
||||
Cluster cluster = init(Cluster.build()
|
||||
.withNodes(nodeCount)
|
||||
.withConfig(conf -> conf.with(GOSSIP, NATIVE_PROTOCOL)
|
||||
.set("dynamic_data_masking_enabled", "true")
|
||||
.set("user_defined_functions_enabled", "true")
|
||||
.set("authenticator", "PasswordAuthenticator")
|
||||
.set("authorizer", "CassandraAuthorizer"))
|
||||
.start());
|
||||
|
||||
// create a user without UNMASK permission
|
||||
withAuthenticatedSession(cluster.get(1), DEFAULT_SUPERUSER_NAME, DEFAULT_SUPERUSER_PASSWORD, session -> {
|
||||
session.execute(format("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'", USERNAME, PASSWORD));
|
||||
session.execute(format("GRANT ALL ON KEYSPACE %s TO %s", KEYSPACE, USERNAME));
|
||||
session.execute(format("REVOKE UNMASK ON KEYSPACE %s FROM %s", KEYSPACE, USERNAME));
|
||||
});
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
private static void assertRowsInAllCoordinators(Cluster cluster, Object[]... expectedRows)
|
||||
{
|
||||
for (int i = 1; i < cluster.size(); i++)
|
||||
{
|
||||
assertRowsWithAuthentication(cluster.get(i), expectedRows);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertRowsWithRestart(IInvokableInstance node, Object[]... expectedRows) throws Throwable
|
||||
{
|
||||
// test querying with in-memory column definitions
|
||||
assertRowsWithAuthentication(node, expectedRows);
|
||||
|
||||
// restart the nodes to reload the column definitions from disk
|
||||
node.shutdown().get();
|
||||
node.startup();
|
||||
|
||||
// test querying with the column definitions loaded from disk
|
||||
assertRowsWithAuthentication(node, expectedRows);
|
||||
}
|
||||
|
||||
private static void assertRowsWithAuthentication(IInvokableInstance node, Object[]... expectedRows)
|
||||
{
|
||||
withAuthenticatedSession(node, USERNAME, PASSWORD, session -> {
|
||||
Statement statement = new SimpleStatement(SELECT).setConsistencyLevel(ConsistencyLevel.ALL);
|
||||
ResultSet resultSet = session.execute(statement);
|
||||
assertRows(RowUtil.toObjects(resultSet), expectedRows);
|
||||
});
|
||||
}
|
||||
|
||||
private static void withAuthenticatedSession(IInvokableInstance instance, String username, String password, Consumer<Session> consumer)
|
||||
{
|
||||
// wait for existing roles
|
||||
await().pollDelay(1, SECONDS)
|
||||
.pollInterval(1, SECONDS)
|
||||
.atMost(1, MINUTES)
|
||||
.until(() -> instance.callOnInstance(CassandraRoleManager::hasExistingRoles));
|
||||
|
||||
// use a load balancing policy that ensures that we actually connect to the desired node
|
||||
InetAddress address = instance.broadcastAddress().getAddress();
|
||||
LoadBalancingPolicy lbc = new SingleHostLoadBalancingPolicy(address);
|
||||
|
||||
Builder builder = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoints(address)
|
||||
.withLoadBalancingPolicy(lbc)
|
||||
.withCredentials(username, password);
|
||||
|
||||
try (com.datastax.driver.core.Cluster cluster = builder.build();
|
||||
Session session = cluster.connect())
|
||||
{
|
||||
consumer.accept(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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.distributed.util;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import com.google.common.collect.Iterators;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.Host;
|
||||
import com.datastax.driver.core.HostDistance;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.policies.LoadBalancingPolicy;
|
||||
|
||||
/**
|
||||
* A CQL driver {@link LoadBalancingPolicy} that only connects to a fixed host.
|
||||
*/
|
||||
public class SingleHostLoadBalancingPolicy implements LoadBalancingPolicy
|
||||
{
|
||||
private final InetAddress address;
|
||||
private Host host;
|
||||
|
||||
public SingleHostLoadBalancingPolicy(InetAddress address)
|
||||
{
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
protected final List<Host> hosts = new CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public void init(Cluster cluster, Collection<Host> hosts)
|
||||
{
|
||||
host = hosts.stream()
|
||||
.filter(h -> h.getBroadcastAddress().equals(address)).findFirst()
|
||||
.orElseThrow(() -> new AssertionError("The host should be a contact point"));
|
||||
this.hosts.add(host);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostDistance distance(Host host)
|
||||
{
|
||||
return HostDistance.LOCAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Host> newQueryPlan(String loggedKeyspace, Statement statement)
|
||||
{
|
||||
return Iterators.singletonIterator(host);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdd(Host host)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUp(Host host)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDown(Host host)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(Host host)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
|
@ -561,7 +561,13 @@ public class AtomicBTreePartitionUpdateBench
|
|||
private static ColumnMetadata[] columns(AbstractType<?> type, ColumnMetadata.Kind kind, int count, String prefix)
|
||||
{
|
||||
return IntStream.range(0, count)
|
||||
.mapToObj(i -> new ColumnMetadata("", "", new ColumnIdentifier(prefix + i, true), type, kind != ColumnMetadata.Kind.REGULAR ? i : ColumnMetadata.NO_POSITION, kind))
|
||||
.mapToObj(i -> new ColumnMetadata("",
|
||||
"",
|
||||
new ColumnIdentifier(prefix + i, true),
|
||||
type,
|
||||
kind != ColumnMetadata.Kind.REGULAR ? i : ColumnMetadata.NO_POSITION,
|
||||
kind,
|
||||
null))
|
||||
.toArray(ColumnMetadata[]::new);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -299,7 +299,8 @@ public class SchemaLoader
|
|||
ColumnIdentifier.getInterned(IntegerType.instance.fromString("42"), IntegerType.instance),
|
||||
UTF8Type.instance,
|
||||
ColumnMetadata.NO_POSITION,
|
||||
ColumnMetadata.Kind.REGULAR);
|
||||
ColumnMetadata.Kind.REGULAR,
|
||||
null);
|
||||
}
|
||||
|
||||
public static ColumnMetadata utf8Column(String ksName, String cfName)
|
||||
|
|
@ -309,7 +310,8 @@ public class SchemaLoader
|
|||
ColumnIdentifier.getInterned("fortytwo", true),
|
||||
UTF8Type.instance,
|
||||
ColumnMetadata.NO_POSITION,
|
||||
ColumnMetadata.Kind.REGULAR);
|
||||
ColumnMetadata.Kind.REGULAR,
|
||||
null);
|
||||
}
|
||||
|
||||
public static TableMetadata perRowIndexedCFMD(String ksName, String cfName)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,9 @@ package org.apache.cassandra.auth;
|
|||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.datastax.driver.core.exceptions.AuthenticationException;
|
||||
import org.apache.cassandra.Util;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mindrot.jbcrypt.BCrypt.gensalt;
|
||||
import static org.mindrot.jbcrypt.BCrypt.hashpw;
|
||||
|
||||
|
|
@ -34,6 +32,10 @@ public class CreateAndAlterRoleTest extends CQLTester
|
|||
@BeforeClass
|
||||
public static void setUpClass()
|
||||
{
|
||||
DatabaseDescriptor.setPermissionsValidity(0);
|
||||
DatabaseDescriptor.setRolesValidity(0);
|
||||
DatabaseDescriptor.setCredentialsValidity(0);
|
||||
|
||||
CQLTester.setUpClass();
|
||||
requireAuthentication();
|
||||
requireNetwork();
|
||||
|
|
@ -62,11 +64,11 @@ public class CreateAndAlterRoleTest extends CQLTester
|
|||
|
||||
useUser(user1, plainTextPwd);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
|
||||
useUser(user2, plainTextPwd);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
|
||||
useSuperUser();
|
||||
|
||||
|
|
@ -78,11 +80,11 @@ public class CreateAndAlterRoleTest extends CQLTester
|
|||
|
||||
useUser(user1, plainTextPwd2);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
|
||||
useUser(user2, plainTextPwd2);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -105,11 +107,11 @@ public class CreateAndAlterRoleTest extends CQLTester
|
|||
|
||||
useUser(user1, plainTextPwd);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
|
||||
useUser(user2, plainTextPwd);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
|
||||
useSuperUser();
|
||||
|
||||
|
|
@ -118,32 +120,10 @@ public class CreateAndAlterRoleTest extends CQLTester
|
|||
|
||||
useUser(user1, plainTextPwd2);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
executeNet("SELECT key FROM system.local");
|
||||
|
||||
useUser(user2, plainTextPwd2);
|
||||
|
||||
executeNetWithAuthSpin("SELECT key FROM system.local");
|
||||
}
|
||||
|
||||
/**
|
||||
* Altering or creating auth may take some time to be effective
|
||||
*
|
||||
* @param query
|
||||
*/
|
||||
void executeNetWithAuthSpin(String query)
|
||||
{
|
||||
Util.spinAssertEquals(true, () -> {
|
||||
try
|
||||
{
|
||||
executeNet(query);
|
||||
return true;
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
assertTrue("Unexpected exception: " + e, e instanceof AuthenticationException);
|
||||
reinitializeNetwork();
|
||||
return false;
|
||||
}
|
||||
}, 10);
|
||||
executeNet("SELECT key FROM system.local");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1276,6 +1276,11 @@ public abstract class CQLTester
|
|||
return sessionNet().execute(new SimpleStatement(formatQuery(query)).setFetchSize(pageSize));
|
||||
}
|
||||
|
||||
protected com.datastax.driver.core.ResultSet executeNetWithoutPaging(String query)
|
||||
{
|
||||
return executeNetWithPaging(query, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
protected Session sessionNet()
|
||||
{
|
||||
return sessionNet(getDefaultVersion());
|
||||
|
|
@ -1721,6 +1726,24 @@ public abstract class CQLTester
|
|||
}
|
||||
}
|
||||
|
||||
protected void assertColumnNames(ResultSet result, String... expectedColumnNames)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
Assert.fail("No rows returned by query.");
|
||||
return;
|
||||
}
|
||||
|
||||
ColumnDefinitions columnDefinitions = result.getColumnDefinitions();
|
||||
Assert.assertEquals("Got less columns than expected.", expectedColumnNames.length, columnDefinitions.size());
|
||||
|
||||
for (int i = 0, m = columnDefinitions.size(); i < m; i++)
|
||||
{
|
||||
String columnName = columnDefinitions.getName(i);
|
||||
Assert.assertEquals(expectedColumnNames[i], columnName);
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertAllRows(Object[]... rows) throws Throwable
|
||||
{
|
||||
assertRows(execute("SELECT * FROM %s"), rows);
|
||||
|
|
@ -1731,6 +1754,11 @@ public abstract class CQLTester
|
|||
return expected;
|
||||
}
|
||||
|
||||
public static Object[][] rows(Object[]... rows)
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
|
||||
protected void assertEmpty(UntypedResultSet result) throws Throwable
|
||||
{
|
||||
if (result != null && !result.isEmpty())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* 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 java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskTester} verifying that masks can be applied to columns in any position (partition key columns,
|
||||
* clustering key columns, static columns and regular columns). The columns of any depending materialized views should
|
||||
* be udpated accordingly.
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public abstract class ColumnMaskInAnyPositionTester extends ColumnMaskTester
|
||||
{
|
||||
/** The column mask as expressed in CQL statements right after the {@code MASKED WITH} keywords. */
|
||||
@Parameterized.Parameter
|
||||
public String mask;
|
||||
|
||||
/** The type of the masked column */
|
||||
@Parameterized.Parameter(1)
|
||||
public String type;
|
||||
|
||||
/** The types of the tested masking function partial arguments. */
|
||||
@Parameterized.Parameter(2)
|
||||
public List<AbstractType<?>> argumentTypes;
|
||||
|
||||
/** The serialized values of the tested masking function partial arguments. */
|
||||
@Parameterized.Parameter(3)
|
||||
public List<ByteBuffer> argumentValues;
|
||||
|
||||
@Test
|
||||
public void testCreateTableWithMaskedColumns() throws Throwable
|
||||
{
|
||||
// Nothing is masked
|
||||
createTable("CREATE TABLE %s (k int, c int, r int, s int static, PRIMARY KEY(k, c))");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r", "s");
|
||||
|
||||
// Masked partition key
|
||||
createTable(format("CREATE TABLE %%s (k %s MASKED WITH %s PRIMARY KEY, r int)", type, mask));
|
||||
assertTableColumnsAreMasked("k");
|
||||
assertTableColumnsAreNotMasked("r");
|
||||
|
||||
// Masked partition key component
|
||||
createTable(format("CREATE TABLE %%s (k1 int, k2 %s MASKED WITH %s, r int, PRIMARY KEY(k1, k2))", type, mask));
|
||||
assertTableColumnsAreMasked("k2");
|
||||
assertTableColumnsAreNotMasked("k1", "r");
|
||||
|
||||
// Masked clustering key
|
||||
createTable(format("CREATE TABLE %%s (k int, c %s MASKED WITH %s, r int, PRIMARY KEY (k, c))", type, mask));
|
||||
assertTableColumnsAreMasked("c");
|
||||
assertTableColumnsAreNotMasked("k", "r");
|
||||
|
||||
// Masked clustering key with reverse order
|
||||
createTable(format("CREATE TABLE %%s (k int, c %s MASKED WITH %s, r int, PRIMARY KEY (k, c)) " +
|
||||
"WITH CLUSTERING ORDER BY (c DESC)", type, mask));
|
||||
assertTableColumnsAreMasked("c");
|
||||
assertTableColumnsAreNotMasked("k", "r");
|
||||
|
||||
// Masked clustering key component
|
||||
createTable(format("CREATE TABLE %%s (k int, c1 int, c2 %s MASKED WITH %s, r int, PRIMARY KEY (k, c1, c2))", type, mask));
|
||||
assertTableColumnsAreMasked("c2");
|
||||
assertTableColumnsAreNotMasked("k", "c1", "r");
|
||||
|
||||
// Masked regular column
|
||||
createTable(format("CREATE TABLE %%s (k int PRIMARY KEY, r1 %s MASKED WITH %s, r2 int)", type, mask));
|
||||
assertTableColumnsAreMasked("r1");
|
||||
assertTableColumnsAreNotMasked("k", "r2");
|
||||
|
||||
// Masked static column
|
||||
createTable(format("CREATE TABLE %%s (k int, c int, r int, s %s STATIC MASKED WITH %s, PRIMARY KEY (k, c))", type, mask));
|
||||
assertTableColumnsAreMasked("s");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r");
|
||||
|
||||
// Multiple masked columns
|
||||
createTable(format("CREATE TABLE %%s (" +
|
||||
"k1 int, k2 %s MASKED WITH %s, " +
|
||||
"c1 int, c2 %s MASKED WITH %s, " +
|
||||
"r1 int, r2 %s MASKED WITH %s, " +
|
||||
"s1 int static, s2 %s static MASKED WITH %s, " +
|
||||
"PRIMARY KEY((k1, k2), c1, c2))",
|
||||
type, mask, type, mask, type, mask, type, mask));
|
||||
assertTableColumnsAreMasked("k2", "c2", "r2", "s2");
|
||||
assertTableColumnsAreNotMasked("k1", "c1", "r1", "s1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateTableWithMaskedColumnsAndMaterializedView() throws Throwable
|
||||
{
|
||||
createTable(format("CREATE TABLE %%s (" +
|
||||
"k1 int, k2 %s MASKED WITH %s, " +
|
||||
"c1 int, c2 %s MASKED WITH %s, " +
|
||||
"r1 int, r2 %s MASKED WITH %s, " +
|
||||
"s1 int static, s2 %s static MASKED WITH %s, " +
|
||||
"PRIMARY KEY((k1, k2), c1, c2))",
|
||||
type, mask, type, mask, type, mask, type, mask));
|
||||
createView("CREATE MATERIALIZED VIEW %s AS SELECT k1, k2, c1, c2, r1, r2 FROM %s " +
|
||||
"WHERE k1 IS NOT NULL AND k2 IS NOT NULL " +
|
||||
"AND c1 IS NOT NULL AND c2 IS NOT NULL " +
|
||||
"AND r1 IS NOT NULL AND r2 IS NOT NULL " +
|
||||
"PRIMARY KEY (r2, c2, c1, k2, k1)");
|
||||
|
||||
assertTableColumnsAreMasked("k2", "c2", "r2", "s2");
|
||||
assertTableColumnsAreNotMasked("k1", "c1", "r1", "s1");
|
||||
|
||||
assertViewColumnsAreMasked("k2", "c2", "r2");
|
||||
assertViewColumnsAreNotMasked("k1", "c1", "r1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlterTableWithMaskedColumns() throws Throwable
|
||||
{
|
||||
// Create the table to be altered
|
||||
createTable(format("CREATE TABLE %%s (k %s, c %<s, r1 %<s, r2 %<s MASKED WITH %s, r3 %s, s %<s static, " +
|
||||
"PRIMARY KEY (k, c))", type, mask, type));
|
||||
assertTableColumnsAreMasked("r2");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r1", "r3", "s");
|
||||
|
||||
// Add new masked column
|
||||
alterTable(format("ALTER TABLE %%s ADD r4 %s MASKED WITH %s", type, mask));
|
||||
assertTableColumnsAreMasked("r2", "r4");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r1", "r3", "s");
|
||||
|
||||
// Set mask for an existing but unmasked column
|
||||
alterTable(format("ALTER TABLE %%s ALTER r1 MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("r1", "r2", "r4");
|
||||
|
||||
// Unmask a masked column
|
||||
alterTable("ALTER TABLE %s ALTER r1 DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER r2 DROP MASKED");
|
||||
assertTableColumnsAreMasked("r4");
|
||||
assertTableColumnsAreNotMasked("r1", "r2", "r3");
|
||||
|
||||
// Mask and disable mask for primary key
|
||||
alterTable(format("ALTER TABLE %%s ALTER k MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("k");
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("k");
|
||||
|
||||
// Mask and disable mask for clustering key
|
||||
alterTable(format("ALTER TABLE %%s ALTER c MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("c");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("c");
|
||||
|
||||
// Mask and disable mask for static column
|
||||
alterTable(format("ALTER TABLE %%s ALTER s MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("s");
|
||||
alterTable("ALTER TABLE %s ALTER s DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("s");
|
||||
|
||||
// Add multiple masked columns within the same query
|
||||
alterTable(format("ALTER TABLE %%s ADD (" +
|
||||
"r5 %s MASKED WITH %s, " +
|
||||
"r6 %s, " +
|
||||
"r7 %s MASKED WITH %s, " +
|
||||
"r8 %s)",
|
||||
type, mask, type, type, mask, type));
|
||||
assertTableColumnsAreMasked("r5", "r7");
|
||||
assertTableColumnsAreNotMasked("r6", "r8");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlterTableWithMaskedColumnsAndMaterializedView() throws Throwable
|
||||
{
|
||||
createTable(format("CREATE TABLE %%s (" +
|
||||
"k %s, c %<s, r1 %<s, r2 %<s, s1 %<s static, s2 %<s static, " +
|
||||
"PRIMARY KEY(k, c))", type));
|
||||
createView("CREATE MATERIALIZED VIEW %s AS SELECT k, c, r2 FROM %s " +
|
||||
"WHERE k IS NOT NULL AND c IS NOT NULL " +
|
||||
"AND r1 IS NOT NULL AND r2 IS NOT NULL " +
|
||||
"PRIMARY KEY (r2, c, k)");
|
||||
|
||||
// Adding a column to the table doesn't have an effect on the view
|
||||
alterTable(format("ALTER TABLE %%s ADD r3 %s MASKED WITH %s", type, mask));
|
||||
assertTableColumnsAreMasked("r3");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r1", "r2", "s1", "s2");
|
||||
assertViewColumnsAreNotMasked("k", "c", "r2");
|
||||
|
||||
// Masking a column that is not part of the view doesn't have an effect on the view
|
||||
alterTable(format("ALTER TABLE %%s ALTER r1 MASKED WITH %s", mask));
|
||||
alterTable(format("ALTER TABLE %%s ALTER s1 MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("r1", "r3", "s1");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r2", "s2");
|
||||
assertViewColumnsAreNotMasked("k", "c", "r2");
|
||||
|
||||
// Masking a column that is part of the view should have an effect on the view
|
||||
alterTable(format("ALTER TABLE %%s ALTER r2 MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("r1", "r2", "r3", "s1");
|
||||
assertTableColumnsAreNotMasked("k", "c", "s2");
|
||||
assertViewColumnsAreMasked("r2");
|
||||
assertViewColumnsAreNotMasked("k", "c");
|
||||
|
||||
// Mask the rest of the columns
|
||||
alterTable(format("ALTER TABLE %%s ALTER k MASKED WITH %s", mask));
|
||||
alterTable(format("ALTER TABLE %%s ALTER c MASKED WITH %s", mask));
|
||||
alterTable(format("ALTER TABLE %%s ALTER s2 MASKED WITH %s", mask));
|
||||
assertTableColumnsAreMasked("k", "c", "r1", "r2", "r3", "s1", "s2");
|
||||
assertViewColumnsAreMasked("k", "c", "r2");
|
||||
|
||||
// Unmask a column that is part of the view
|
||||
alterTable("ALTER TABLE %s ALTER r2 DROP MASKED");
|
||||
assertTableColumnsAreMasked("k", "c", "r1", "r3", "s1", "s2");
|
||||
assertTableColumnsAreNotMasked("r2");
|
||||
assertViewColumnsAreMasked("k", "c");
|
||||
assertViewColumnsAreNotMasked("r2");
|
||||
|
||||
// Unmask the rest of the columns
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER r1 DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER r3 DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER s1 DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER s2 DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r1", "r2", "r3", "s1", "s2");
|
||||
assertViewColumnsAreNotMasked("k", "c", "r2");
|
||||
}
|
||||
|
||||
private String functionName()
|
||||
{
|
||||
if (mask.equals("DEFAULT"))
|
||||
return "mask_default";
|
||||
|
||||
return StringUtils.remove(StringUtils.substringBefore(mask, "("), KEYSPACE + ".");
|
||||
}
|
||||
|
||||
private void assertTableColumnsAreMasked(String... columns) throws Throwable
|
||||
{
|
||||
for (String column : columns)
|
||||
{
|
||||
assertColumnIsMasked(currentTable(), column, functionName(), argumentTypes, argumentValues);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertViewColumnsAreMasked(String... columns) throws Throwable
|
||||
{
|
||||
for (String column : columns)
|
||||
{
|
||||
assertColumnIsMasked(currentView(), column, functionName(), argumentTypes, argumentValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskInAnyPositionTester} for {@link DefaultMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskInAnyPositionWithDefaultTest extends ColumnMaskInAnyPositionTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "mask={0}, type={1}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "DEFAULT", "int", emptyList(), emptyList() },
|
||||
{ "DEFAULT", "text", emptyList(), emptyList() },
|
||||
{ "DEFAULT", "frozen<list<uuid>>", emptyList(), emptyList() },
|
||||
{ "mask_default()", "int", emptyList(), emptyList() },
|
||||
{ "mask_default()", "text", emptyList(), emptyList() },
|
||||
{ "mask_default()", "frozen<list<uuid>>", emptyList(), emptyList() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskInAnyPositionTester} for {@link NullMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskInAnyPositionWithNullTest extends ColumnMaskInAnyPositionTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "mask={0}, type={1}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "mask_null()", "int", emptyList(), emptyList() },
|
||||
{ "mask_null()", "text", emptyList(), emptyList() },
|
||||
{ "mask_null()", "frozen<list<uuid>>", emptyList(), emptyList() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskInAnyPositionTester} for {@link PartialMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskInAnyPositionWithPartialTest extends ColumnMaskInAnyPositionTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "mask={0}, type={1}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "mask_inner(1, 2)", "text",
|
||||
asList(Int32Type.instance, Int32Type.instance),
|
||||
asList(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2)) },
|
||||
{ "mask_outer(1, 2)", "text",
|
||||
asList(Int32Type.instance, Int32Type.instance),
|
||||
asList(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2)) },
|
||||
{ "mask_inner(1, 2, '#')", "text",
|
||||
asList(Int32Type.instance, Int32Type.instance, UTF8Type.instance),
|
||||
asList(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2), UTF8Type.instance.decompose("#")) },
|
||||
{ "mask_outer(1, 2, '#')", "text",
|
||||
asList(Int32Type.instance, Int32Type.instance, UTF8Type.instance),
|
||||
asList(Int32Type.instance.decompose(1), Int32Type.instance.decompose(2), UTF8Type.instance.decompose("#")) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.ListType;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskInAnyPositionTester} for {@link ReplaceMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskInAnyPositionWithReplaceTest extends ColumnMaskInAnyPositionTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "mask={0}, type={1}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "mask_replace(0)", "int",
|
||||
singletonList(Int32Type.instance),
|
||||
singletonList(Int32Type.instance.decompose(0)) },
|
||||
{ "mask_replace('redacted')", "text",
|
||||
singletonList(UTF8Type.instance),
|
||||
singletonList(UTF8Type.instance.decompose("redacted")) },
|
||||
{ "mask_replace([])", "frozen<list<int>>",
|
||||
singletonList(ListType.getInstance(Int32Type.instance, false)),
|
||||
singletonList(ListType.getInstance(Int32Type.instance, false).decompose(emptyList())) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskInAnyPositionTester} for user-defined functions.
|
||||
*/
|
||||
public class ColumnMaskInAnyPositionWithUDFTest extends ColumnMaskInAnyPositionTester
|
||||
{
|
||||
private static final String TEXT_REPLACE_FUNCTION = KEYSPACE + ".mask_text_replace";
|
||||
private static final String INT_REPLACE_FUNCTION = KEYSPACE + ".mask_int_replace";
|
||||
|
||||
@Before
|
||||
public void setupSchema() throws Throwable
|
||||
{
|
||||
createFunction(KEYSPACE,
|
||||
"text, text",
|
||||
"CREATE FUNCTION IF NOT EXISTS " + TEXT_REPLACE_FUNCTION + " (column text, replacement text) " +
|
||||
"CALLED ON NULL INPUT " +
|
||||
"RETURNS text " +
|
||||
"LANGUAGE java " +
|
||||
"AS 'return replacement;'");
|
||||
createFunction(KEYSPACE,
|
||||
"int, int",
|
||||
"CREATE FUNCTION IF NOT EXISTS " + INT_REPLACE_FUNCTION + " (column int, replacement int) " +
|
||||
"CALLED ON NULL INPUT " +
|
||||
"RETURNS int " +
|
||||
"LANGUAGE java " +
|
||||
"AS 'return replacement;'");
|
||||
}
|
||||
|
||||
@Parameterized.Parameters(name = "mask={0}, type={1}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ TEXT_REPLACE_FUNCTION + "('redacted')", "text",
|
||||
singletonList(UTF8Type.instance),
|
||||
singletonList(UTF8Type.instance.decompose("redacted")) },
|
||||
{ INT_REPLACE_FUNCTION + "(0)", "int",
|
||||
singletonList(Int32Type.instance),
|
||||
singletonList(Int32Type.instance.decompose(0)) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import org.apache.cassandra.cql3.CQL3Type;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskTester} verifying that we can attach column masks to table columns with any native data type.
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ColumnMaskNativeTypesTest extends ColumnMaskTester
|
||||
{
|
||||
/** The type of the column. */
|
||||
@Parameterized.Parameter
|
||||
public CQL3Type.Native type;
|
||||
|
||||
@Parameterized.Parameters(name = "type={0}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
List<Object[]> parameters = new ArrayList<>();
|
||||
for (CQL3Type.Native type : CQL3Type.Native.values())
|
||||
{
|
||||
if (type != CQL3Type.Native.EMPTY)
|
||||
parameters.add(new Object[]{ type });
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNativeDataTypes() throws Throwable
|
||||
{
|
||||
String def = format("%s MASKED WITH DEFAULT", type);
|
||||
String keyDef = type == CQL3Type.Native.COUNTER || type == CQL3Type.Native.DURATION
|
||||
? "int MASKED WITH DEFAULT" : def;
|
||||
String staticDef = format("%s STATIC MASKED WITH DEFAULT", type);
|
||||
|
||||
// Create table with masks
|
||||
String table = createTable(format("CREATE TABLE %%s (k %s, c %<s, r %s, s %s, PRIMARY KEY(k, c))", keyDef, def, staticDef));
|
||||
assertColumnIsMasked(table, "k", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "c", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "r", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "s", "mask_default", emptyList(), emptyList());
|
||||
|
||||
// Alter column masks
|
||||
alterTable("ALTER TABLE %s ALTER k MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER c MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER r MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER s MASKED WITH mask_null()");
|
||||
assertColumnIsMasked(table, "k", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "c", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "r", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "s", "mask_null", emptyList(), emptyList());
|
||||
|
||||
// Drop masks
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER r DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER s DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("k", "c", "r", "s");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import com.datastax.driver.core.ResultSet;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
/**
|
||||
* Test queries on columns that have attached a dynamic data masking function.
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public abstract class ColumnMaskQueryTester extends ColumnMaskTester
|
||||
{
|
||||
@Parameterized.Parameter
|
||||
public String order;
|
||||
|
||||
@Parameterized.Parameter(1)
|
||||
public String mask;
|
||||
|
||||
@Parameterized.Parameter(2)
|
||||
public String columnType;
|
||||
|
||||
@Parameterized.Parameter(3)
|
||||
public Object columnValue;
|
||||
|
||||
@Parameterized.Parameter(4)
|
||||
public Object maskedValue;
|
||||
|
||||
@Before
|
||||
public void setupSchema() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (" +
|
||||
format("k1 %s, k2 %<s MASKED WITH %s, ", columnType, mask) +
|
||||
format("c1 %s, c2 %<s MASKED WITH %s, ", columnType, mask) +
|
||||
format("r1 %s, r2 %<s MASKED WITH %s, ", columnType, mask) +
|
||||
format("s1 %s static, s2 %<s static MASKED WITH %s, ", columnType, mask) +
|
||||
format("PRIMARY KEY((k1, k2), c1, c2)) WITH CLUSTERING ORDER BY (c1 %s, c2 %<s)", order));
|
||||
|
||||
createView("CREATE MATERIALIZED VIEW %s AS SELECT k2, k1, c2, c1, r2, r1 FROM %s " +
|
||||
"WHERE k1 IS NOT NULL AND k2 IS NOT NULL " +
|
||||
"AND c1 IS NOT NULL AND c2 IS NOT NULL " +
|
||||
"AND r1 IS NOT NULL AND r2 IS NOT NULL " +
|
||||
"PRIMARY KEY ((c2, c1), k2, k1)");
|
||||
|
||||
executeNet("INSERT INTO %s(k1, k2, c1, c2, r1, r2, s1, s2) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
columnValue, columnValue, columnValue, columnValue, columnValue, columnValue, columnValue, columnValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectWithWilcard() throws Throwable
|
||||
{
|
||||
ResultSet rs = executeNet("SELECT * FROM %s");
|
||||
assertColumnNames(rs, "k1", "k2", "c1", "c2", "s1", "s2", "r1", "r2");
|
||||
assertRowsNet(rs, row(columnValue, maskedValue,
|
||||
columnValue, maskedValue,
|
||||
columnValue, maskedValue,
|
||||
columnValue, maskedValue));
|
||||
|
||||
rs = executeNet(format("SELECT * FROM %s.%s", KEYSPACE, currentView()));
|
||||
assertColumnNames(rs, "c2", "c1", "k2", "k1", "r1", "r2");
|
||||
assertRowsNet(rs, row(maskedValue, columnValue,
|
||||
maskedValue, columnValue,
|
||||
columnValue, maskedValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectWithAllColumnNames() throws Throwable
|
||||
{
|
||||
ResultSet rs = executeNet("SELECT c2, c1, k2, k1, r2, r1, s2, s1 FROM %s");
|
||||
assertColumnNames(rs, "c2", "c1", "k2", "k1", "r2", "r1", "s2", "s1");
|
||||
assertRowsNet(rs, row(maskedValue, columnValue,
|
||||
maskedValue, columnValue,
|
||||
maskedValue, columnValue,
|
||||
maskedValue, columnValue));
|
||||
|
||||
rs = executeNet(format("SELECT c2, c1, k2, k1, r2, r1 FROM %s.%s", KEYSPACE, currentView()));
|
||||
assertColumnNames(rs, "c2", "c1", "k2", "k1", "r2", "r1");
|
||||
assertRowsNet(rs, row(maskedValue, columnValue,
|
||||
maskedValue, columnValue,
|
||||
maskedValue, columnValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectOnlyMaskedColumns() throws Throwable
|
||||
{
|
||||
ResultSet rs = executeNet("SELECT k2, c2, s2, r2 FROM %s");
|
||||
assertColumnNames(rs, "k2", "c2", "s2", "r2");
|
||||
assertRowsNet(rs, row(maskedValue, maskedValue, maskedValue, maskedValue));
|
||||
|
||||
rs = executeNet(format("SELECT k2, c2, r2 FROM %s.%s", KEYSPACE, currentView()));
|
||||
assertColumnNames(rs, "k2", "c2", "r2");
|
||||
assertRowsNet(rs, row(maskedValue, maskedValue, maskedValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectOnlyNotMaskedColumns() throws Throwable
|
||||
{
|
||||
ResultSet rs = executeNet("SELECT k1, c1, s1, r1 FROM %s");
|
||||
assertColumnNames(rs, "k1", "c1", "s1", "r1");
|
||||
assertRowsNet(rs, row(columnValue, columnValue, columnValue, columnValue));
|
||||
|
||||
rs = executeNet(format("SELECT k1, c1, r1 FROM %s.%s", KEYSPACE, currentView()));
|
||||
assertColumnNames(rs, "k1", "c1", "r1");
|
||||
assertRowsNet(rs, row(columnValue, columnValue, columnValue));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskQueryTester} for {@link DefaultMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskQueryWithDefaultTest extends ColumnMaskQueryTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "order={0}, mask={1}, type={2}, value={3}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
List<Object[]> options = new ArrayList<>();
|
||||
for (String order : Arrays.asList("ASC", "DESC"))
|
||||
{
|
||||
options.add(new Object[]{ order, "DEFAULT", "text", "abc", "****" });
|
||||
options.add(new Object[]{ order, "DEFAULT", "int", 123, 0 });
|
||||
options.add(new Object[]{ order, "mask_default()", "text", "abc", "****" });
|
||||
options.add(new Object[]{ order, "mask_default()", "int", 123, 0, });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskQueryTester} for {@link NullMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskQueryWithNullTest extends ColumnMaskQueryTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "order={0}, mask={1}, type={2}, value={3}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
List<Object[]> options = new ArrayList<>();
|
||||
for (String order : Arrays.asList("ASC", "DESC"))
|
||||
{
|
||||
options.add(new Object[]{ order, "mask_null()", "text", "abc", null });
|
||||
options.add(new Object[]{ order, "mask_null()", "int", 123, null });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskQueryTester} for {@link PartialMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskQueryWithPartialTest extends ColumnMaskQueryTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "order={0}, mask={1}, type={2}, value={3}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
List<Object[]> options = new ArrayList<>();
|
||||
for (String order : Arrays.asList("ASC", "DESC"))
|
||||
{
|
||||
options.add(new Object[]{ order, "mask_inner(null, null)", "text", "abcdef", "******" });
|
||||
options.add(new Object[]{ order, "mask_inner(1, 2)", "text", "abcdef", "a***ef" });
|
||||
options.add(new Object[]{ order, "mask_inner(1, 2, '#')", "text", "abcdef", "a###ef" });
|
||||
options.add(new Object[]{ order, "mask_outer(1, null)", "text", "abcdef", "*bcdef" });
|
||||
options.add(new Object[]{ order, "mask_outer(1, 2)", "text", "abcdef", "*bcd**" });
|
||||
options.add(new Object[]{ order, "mask_outer(1, 2, '#')", "text", "abcdef", "#bcd##", });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskQueryTester} for {@link ReplaceMaskingFunction}.
|
||||
*/
|
||||
public class ColumnMaskQueryWithReplaceTest extends ColumnMaskQueryTester
|
||||
{
|
||||
@Parameterized.Parameters(name = "order={0}, mask={1}, type={2}, value={3}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
List<Object[]> options = new ArrayList<>();
|
||||
for (String order : Arrays.asList("ASC", "DESC"))
|
||||
{
|
||||
options.add(new Object[]{ order, "mask_replace(null)", "int", 123, null });
|
||||
options.add(new Object[]{ order, "mask_replace('redacted')", "ascii", "abc", "redacted" });
|
||||
options.add(new Object[]{ order, "mask_replace('redacted')", "text", "abc", "redacted" });
|
||||
options.add(new Object[]{ order, "mask_replace(0)", "int", 123, 0 });
|
||||
options.add(new Object[]{ order, "mask_replace(0)", "bigint", 123L, 0L });
|
||||
options.add(new Object[]{ order, "mask_replace(0)", "varint", BigInteger.valueOf(123), BigInteger.ZERO });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskQueryTester} for user-defined functions.
|
||||
*/
|
||||
public class ColumnMaskQueryWithUDFTest extends ColumnMaskQueryTester
|
||||
{
|
||||
private static final String TEXT_REPLACE_FUNCTION = KEYSPACE + ".mask_text_replace";
|
||||
private static final String INT_REPLACE_FUNCTION = KEYSPACE + ".mask_int_replace";
|
||||
|
||||
@Before
|
||||
@Override
|
||||
public void setupSchema() throws Throwable
|
||||
{
|
||||
createFunction(KEYSPACE,
|
||||
"text, text",
|
||||
"CREATE FUNCTION IF NOT EXISTS " + TEXT_REPLACE_FUNCTION + " (column text, replacement text) " +
|
||||
"CALLED ON NULL INPUT " +
|
||||
"RETURNS text " +
|
||||
"LANGUAGE java " +
|
||||
"AS 'return replacement;'");
|
||||
createFunction(KEYSPACE,
|
||||
"int, int",
|
||||
"CREATE FUNCTION IF NOT EXISTS " + INT_REPLACE_FUNCTION + " (column int, replacement int) " +
|
||||
"CALLED ON NULL INPUT " +
|
||||
"RETURNS int " +
|
||||
"LANGUAGE java " +
|
||||
"AS 'return replacement;'");
|
||||
super.setupSchema();
|
||||
}
|
||||
|
||||
@Parameterized.Parameters(name = "order={0}, mask={1}, type={2}, value={3}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
List<Object[]> options = new ArrayList<>();
|
||||
for (String order : Arrays.asList("ASC", "DESC"))
|
||||
{
|
||||
options.add(new Object[]{ order, TEXT_REPLACE_FUNCTION + "('redacted')", "text", "abc", "redacted" });
|
||||
options.add(new Object[]{ order, TEXT_REPLACE_FUNCTION + "('secret')", "text", "abc", "secret" });
|
||||
options.add(new Object[]{ order, INT_REPLACE_FUNCTION + "(0)", "int", 123, 0 });
|
||||
options.add(new Object[]{ order, INT_REPLACE_FUNCTION + "(-1)", "int", 123, -1 });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,580 @@
|
|||
/*
|
||||
* 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 java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.datastax.driver.core.BoundStatement;
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.Session;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQL3Type;
|
||||
import org.apache.cassandra.cql3.functions.FunctionFactory;
|
||||
import org.apache.cassandra.cql3.functions.FunctionParameter;
|
||||
import org.apache.cassandra.cql3.functions.NativeFunction;
|
||||
import org.apache.cassandra.cql3.functions.NativeFunctions;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.apache.cassandra.cql3.functions.masking.ColumnMask.DISABLED_ERROR_MESSAGE;
|
||||
|
||||
/**
|
||||
* Tests schema altering queries ({@code CREATE TABLE}, {@code ALTER TABLE}, etc.) that attach/dettach dynamic data
|
||||
* masking functions to column definitions.
|
||||
*/
|
||||
public class ColumnMaskTest extends ColumnMaskTester
|
||||
{
|
||||
@Test
|
||||
public void testCollections() throws Throwable
|
||||
{
|
||||
// Create table with masks
|
||||
String table = createTable("CREATE TABLE %s (k int PRIMARY KEY, " +
|
||||
"s set<int> MASKED WITH DEFAULT, " +
|
||||
"l list<int> MASKED WITH DEFAULT, " +
|
||||
"m map<int, int> MASKED WITH DEFAULT, " +
|
||||
"fs frozen<set<int>> MASKED WITH DEFAULT, " +
|
||||
"fl frozen<list<int>> MASKED WITH DEFAULT, " +
|
||||
"fm frozen<map<int, int>> MASKED WITH DEFAULT)");
|
||||
assertColumnIsMasked(table, "s", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "l", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "m", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "fs", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "fl", "mask_default", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "fm", "mask_default", emptyList(), emptyList());
|
||||
|
||||
// Alter column masks
|
||||
alterTable("ALTER TABLE %s ALTER s MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER l MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER m MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER fs MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER fl MASKED WITH mask_null()");
|
||||
alterTable("ALTER TABLE %s ALTER fm MASKED WITH mask_null()");
|
||||
assertColumnIsMasked(table, "s", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "l", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "m", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "fs", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "fl", "mask_null", emptyList(), emptyList());
|
||||
assertColumnIsMasked(table, "fm", "mask_null", emptyList(), emptyList());
|
||||
|
||||
// Drop masks
|
||||
alterTable("ALTER TABLE %s ALTER s DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER l DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER m DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER fs DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER fl DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER fm DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("s", "l", "m", "fs", "fl", "fm");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUDTs() throws Throwable
|
||||
{
|
||||
String type = createType("CREATE TYPE %s (a1 varint, a2 varint, a3 varint);");
|
||||
|
||||
// Create table with mask
|
||||
String table = createTable(format("CREATE TABLE %%s (k int PRIMARY KEY, v %s MASKED WITH DEFAULT)", type));
|
||||
assertColumnIsMasked(table, "v", "mask_default", emptyList(), emptyList());
|
||||
|
||||
// Alter column mask
|
||||
alterTable("ALTER TABLE %s ALTER v MASKED WITH mask_null()");
|
||||
assertColumnIsMasked(table, "v", "mask_null", emptyList(), emptyList());
|
||||
|
||||
// Drop mask
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("v");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlterTableAddMaskingToNonExistingColumn() throws Throwable
|
||||
{
|
||||
String table = createTable("CREATE TABLE %s (k int PRIMARY KEY, v text)");
|
||||
execute("ALTER TABLE %s ALTER IF EXISTS unknown MASKED WITH DEFAULT");
|
||||
assertInvalidMessage(format("Column with name 'unknown' doesn't exist on table '%s'", table),
|
||||
formatQuery("ALTER TABLE %s ALTER unknown MASKED WITH DEFAULT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlterTableRemoveMaskingFromNonExistingColumn() throws Throwable
|
||||
{
|
||||
String table = createTable("CREATE TABLE %s (k int PRIMARY KEY, v text)");
|
||||
execute("ALTER TABLE %s ALTER IF EXISTS unknown DROP MASKED");
|
||||
assertInvalidMessage(format("Column with name 'unknown' doesn't exist on table '%s'", table),
|
||||
formatQuery("ALTER TABLE %s ALTER unknown DROP MASKED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlterTableRemoveMaskFromUnmaskedColumn() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int PRIMARY KEY, v text)");
|
||||
execute("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertTableColumnsAreNotMasked("v");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidMaskingFunctionName() throws Throwable
|
||||
{
|
||||
// create table
|
||||
createTableName();
|
||||
assertInvalidMessage("Unable to find masking function for v, no declared function matches the signature mask_missing()",
|
||||
formatQuery("CREATE TABLE %s (k int PRIMARY KEY, v int MASKED WITH mask_missing())"));
|
||||
|
||||
// alter table
|
||||
createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
|
||||
assertInvalidMessage("Unable to find masking function for v, no declared function matches the signature mask_missing()",
|
||||
"ALTER TABLE %s ALTER v MASKED WITH mask_missing()");
|
||||
|
||||
assertTableColumnsAreNotMasked("k", "v");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidMaskingFunctionArguments() throws Throwable
|
||||
{
|
||||
// create table
|
||||
createTableName();
|
||||
assertInvalidMessage("Invalid number of arguments for function system.mask_default(any)",
|
||||
formatQuery("CREATE TABLE %s (k int PRIMARY KEY, v int MASKED WITH mask_default(1))"));
|
||||
|
||||
// alter table
|
||||
createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)");
|
||||
assertInvalidMessage("Invalid number of arguments for function system.mask_default(any)",
|
||||
"ALTER TABLE %s ALTER v MASKED WITH mask_default(1)");
|
||||
|
||||
assertTableColumnsAreNotMasked("k", "v");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidMaskingFunctionArgumentTypes() throws Throwable
|
||||
{
|
||||
// create table
|
||||
createTableName();
|
||||
assertInvalidMessage("Function system.mask_inner requires an argument of type int, but found argument 'a' of type ascii",
|
||||
formatQuery("CREATE TABLE %s (k int PRIMARY KEY, v text MASKED WITH mask_inner('a', 'b'))"));
|
||||
|
||||
// alter table
|
||||
createTable("CREATE TABLE %s (k int PRIMARY KEY, v text)");
|
||||
assertInvalidMessage("Function system.mask_inner requires an argument of type int, but found argument 'a' of type ascii",
|
||||
"ALTER TABLE %s ALTER v MASKED WITH mask_inner('a', 'b')");
|
||||
assertTableColumnsAreNotMasked("k", "v");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testColumnMaskingWithNotMaskingFunction() throws Throwable
|
||||
{
|
||||
// create table
|
||||
createTableName();
|
||||
assertInvalidMessage("Not-masking function tojson() cannot be used for masking table columns",
|
||||
formatQuery("CREATE TABLE %s (k int PRIMARY KEY, v text MASKED WITH tojson())"));
|
||||
|
||||
// alter table
|
||||
createTable("CREATE TABLE %s (k int PRIMARY KEY, v text)");
|
||||
assertInvalidMessage("Not-masking function tojson() cannot be used for masking table columns",
|
||||
"ALTER TABLE %s ALTER v MASKED WITH tojson()");
|
||||
assertTableColumnsAreNotMasked("k", "v");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testPreparedStatement() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int PRIMARY KEY, v text MASKED WITH DEFAULT)");
|
||||
execute("INSERT INTO %s (k, v) VALUES (0, 'sensitive')");
|
||||
|
||||
Session session = sessionNet();
|
||||
PreparedStatement prepared = session.prepare(formatQuery("SELECT v FROM %s WHERE k = ?"));
|
||||
BoundStatement bound = prepared.bind(0);
|
||||
assertRowsNet(session.execute(bound), row("****"));
|
||||
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertRowsNet(session.execute(bound), row("sensitive"));
|
||||
|
||||
alterTable("ALTER TABLE %s ALTER v MASKED WITH mask_replace('redacted')");
|
||||
assertRowsNet(session.execute(bound), row("redacted"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testViews() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int, c int, v text MASKED WITH mask_replace('redacted'), PRIMARY KEY (k, c))");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 'sensitive')");
|
||||
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)");
|
||||
waitForViewMutations();
|
||||
assertRowsNet(executeNet(format("SELECT v FROM %s.%s", KEYSPACE, view)), row("redacted"));
|
||||
assertRowsNet(executeNet(format("SELECT v FROM %s.%s WHERE v='sensitive'", KEYSPACE, view)), row("redacted"));
|
||||
assertRowsNet(executeNet(format("SELECT v FROM %s.%s WHERE v='redacted'", KEYSPACE, view)));
|
||||
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertRowsNet(executeNet(format("SELECT v FROM %s.%s", KEYSPACE, view)), row("sensitive"));
|
||||
assertRowsNet(executeNet(format("SELECT v FROM %s.%s WHERE v='sensitive'", KEYSPACE, view)), row("sensitive"));
|
||||
assertRowsNet(executeNet(format("SELECT v FROM %s.%s WHERE v='redacted'", KEYSPACE, view)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void testPreparedStatementOnView() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int, c int, v text MASKED WITH DEFAULT, PRIMARY KEY (k, c))");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 'sensitive')");
|
||||
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)");
|
||||
waitForViewMutations();
|
||||
|
||||
Session session = sessionNet();
|
||||
PreparedStatement prepared = session.prepare(format("SELECT v FROM %s.%s WHERE v=?", KEYSPACE, view));
|
||||
BoundStatement bound = prepared.bind("sensitive");
|
||||
assertRowsNet(session.execute(bound), row("****"));
|
||||
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertRowsNet(session.execute(bound), row("sensitive"));
|
||||
|
||||
alterTable("ALTER TABLE %s ALTER v MASKED WITH mask_replace('redacted')");
|
||||
assertRowsNet(session.execute(bound), row("redacted"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupBy() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int, c int, v text, PRIMARY KEY (k, c))");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 'sensitive')");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 'sensitive')");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (1, 0, 'sensitive')");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (1, 1, 'sensitive')");
|
||||
|
||||
// without masks
|
||||
String query = "SELECT * FROM %s GROUP BY k";
|
||||
assertRowsNet(executeNet(query), row(1, 0, "sensitive"), row(0, 0, "sensitive"));
|
||||
|
||||
// with masked regular column
|
||||
alterTable("ALTER TABLE %s ALTER v MASKED WITH mask_replace('redacted')");
|
||||
assertRowsNet(executeNet(query), row(1, 0, "redacted"), row(0, 0, "redacted"));
|
||||
|
||||
// with masked clustering key
|
||||
alterTable("ALTER TABLE %s ALTER c MASKED WITH mask_replace(-1)");
|
||||
assertRowsNet(executeNet(query), row(1, -1, "redacted"), row(0, -1, "redacted"));
|
||||
|
||||
// with masked partition key
|
||||
alterTable("ALTER TABLE %s ALTER k MASKED WITH mask_replace(-1)");
|
||||
assertRowsNet(executeNet(query), row(-1, -1, "redacted"), row(-1, -1, "redacted"));
|
||||
|
||||
// again without masks
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertRowsNet(executeNet(query), row(1, 0, "sensitive"), row(0, 0, "sensitive"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPaging() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int, c int, v text, PRIMARY KEY (k, c))");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 'sensitive')");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 'sensitive')");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (1, 0, 'sensitive')");
|
||||
|
||||
// without masks
|
||||
assertRowsWithPaging("SELECT * FROM %s", row(1, 0, "sensitive"), row(0, 0, "sensitive"), row(0, 1, "sensitive"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 1", row(1, 0, "sensitive"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0", row(0, 0, "sensitive"), row(0, 1, "sensitive"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0 AND c = 1", row(0, 1, "sensitive"));
|
||||
|
||||
// with masked regular column
|
||||
alterTable("ALTER TABLE %s ALTER v MASKED WITH mask_replace('redacted')");
|
||||
assertRowsWithPaging("SELECT * FROM %s", row(1, 0, "redacted"), row(0, 0, "redacted"), row(0, 1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 1", row(1, 0, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0", row(0, 0, "redacted"), row(0, 1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0 AND c = 1", row(0, 1, "redacted"));
|
||||
|
||||
// with masked clustering key
|
||||
alterTable("ALTER TABLE %s ALTER c MASKED WITH mask_replace(-1)");
|
||||
assertRowsWithPaging("SELECT * FROM %s", row(1, -1, "redacted"), row(0, -1, "redacted"), row(0, -1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 1", row(1, -1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0", row(0, -1, "redacted"), row(0, -1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0 AND c = 1", row(0, -1, "redacted"));
|
||||
|
||||
// with masked partition key
|
||||
alterTable("ALTER TABLE %s ALTER k MASKED WITH mask_replace(-1)");
|
||||
assertRowsWithPaging("SELECT * FROM %s", row(-1, -1, "redacted"), row(-1, -1, "redacted"), row(-1, -1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 1", row(-1, -1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0", row(-1, -1, "redacted"), row(-1, -1, "redacted"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0 AND c = 1", row(-1, -1, "redacted"));
|
||||
|
||||
// again without masks
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
assertRowsWithPaging("SELECT * FROM %s", row(1, 0, "sensitive"), row(0, 0, "sensitive"), row(0, 1, "sensitive"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 1", row(1, 0, "sensitive"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0", row(0, 0, "sensitive"), row(0, 1, "sensitive"));
|
||||
assertRowsWithPaging("SELECT * FROM %s WHERE k = 0 AND c = 1", row(0, 1, "sensitive"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that rows are always ordered according to the clear values of the columns, even for the post-ordering done
|
||||
* for queries with {@code IN} restrictions and {@code ORDER BY} clauses.
|
||||
*/
|
||||
@Test
|
||||
public void testPostOrdering() throws Throwable
|
||||
{
|
||||
createTable("CREATE TABLE %s (k int, c int, v int, PRIMARY KEY (k, c))");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 0)");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 1)");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (1, 0, 3)");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (1, 1, 4)");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (2, 0, 6)");
|
||||
execute("INSERT INTO %s (k, c, v) VALUES (2, 1, 7)");
|
||||
NativeFunctions.instance.add(NEGATIVE);
|
||||
|
||||
// Test ordering without masking, just for reference
|
||||
assertRowsNet(executeNet("SELECT * FROM %s WHERE k IN (0, 1, 2)"),
|
||||
row(0, 0, 0),
|
||||
row(0, 1, 1),
|
||||
row(1, 0, 3),
|
||||
row(1, 1, 4),
|
||||
row(2, 0, 6),
|
||||
row(2, 1, 7));
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT * FROM %s WHERE k IN (0, 1, 2) ORDER BY c ASC"),
|
||||
row(0, 0, 0),
|
||||
row(1, 0, 3),
|
||||
row(2, 0, 6),
|
||||
row(0, 1, 1),
|
||||
row(1, 1, 4),
|
||||
row(2, 1, 7));
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT * FROM %s WHERE k IN (0, 1, 2) ORDER BY c DESC"),
|
||||
row(0, 1, 1),
|
||||
row(1, 1, 4),
|
||||
row(2, 1, 7),
|
||||
row(0, 0, 0),
|
||||
row(1, 0, 3),
|
||||
row(2, 0, 6));
|
||||
|
||||
// Test ordering with manually applied masking function, just for reference
|
||||
assertRowsNet(executeNet("SELECT k, mask_negative(c), v FROM %s WHERE k IN (0, 1, 2)"),
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -0, 6), // (2, 0, 6)
|
||||
row(2, -1, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT k, mask_negative(c), v FROM %s WHERE k IN (0, 1, 2) ORDER BY c ASC"),
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(2, -0, 6), // (2, 0, 6)
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -1, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT k, mask_negative(c), v FROM %s WHERE k IN (0, 1, 2) ORDER BY c DESC"),
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -1, 7), // (2, 1, 7)
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(2, -0, 6)); // (2, 0, 6)
|
||||
|
||||
alterTable("ALTER TABLE %s ALTER c MASKED WITH mask_negative()");
|
||||
|
||||
// Test ordering of wildcard queries with masked column
|
||||
assertRowsNet(executeNet("SELECT * FROM %s WHERE k IN (0, 1, 2)"),
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -0, 6), // (2, 0, 6)
|
||||
row(2, -1, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT * FROM %s WHERE k IN (0, 1, 2) ORDER BY c ASC"),
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(2, -0, 6), // (2, 0, 6)
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -1, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT * FROM %s WHERE k IN (0, 1, 2) ORDER BY c DESC"),
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -1, 7), // (2, 1, 7)
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(2, -0, 6)); // (2, 0, 6)
|
||||
|
||||
// Test ordering of column selection queries with masked column
|
||||
assertRowsNet(executeNet("SELECT k, c, v FROM %s WHERE k IN (0, 1, 2)"),
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -0, 6), // (2, 0, 6)
|
||||
row(2, -1, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT k, c, v FROM %s WHERE k IN (0, 1, 2) ORDER BY c ASC"),
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(2, -0, 6), // (2, 0, 6)
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -1, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT k, c, v FROM %s WHERE k IN (0, 1, 2) ORDER BY c DESC"),
|
||||
row(0, -1, 1), // (0, 1, 1)
|
||||
row(1, -1, 4), // (1, 1, 4)
|
||||
row(2, -1, 7), // (2, 1, 7)
|
||||
row(0, -0, 0), // (0, 0, 0)
|
||||
row(1, -0, 3), // (1, 0, 3)
|
||||
row(2, -0, 6)); // (2, 0, 6)
|
||||
|
||||
// Test ordering of column selection queries with masked column, without selecting the ordered column
|
||||
assertRowsNet(executeNet("SELECT k, v FROM %s WHERE k IN (0, 1, 2)"),
|
||||
row(0, 0), // (0, 0, 0)
|
||||
row(0, 1), // (0, 1, 1)
|
||||
row(1, 3), // (1, 0, 3)
|
||||
row(1, 4), // (1, 1, 4)
|
||||
row(2, 6), // (2, 0, 6)
|
||||
row(2, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT k, v FROM %s WHERE k IN (0, 1, 2) ORDER BY c ASC"),
|
||||
row(0, 0), // (0, 0, 0)
|
||||
row(1, 3), // (1, 0, 3)
|
||||
row(2, 6), // (2, 0, 6)
|
||||
row(0, 1), // (0, 1, 1)
|
||||
row(1, 4), // (1, 1, 4)
|
||||
row(2, 7)); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT k, v FROM %s WHERE k IN (0, 1, 2) ORDER BY c DESC"),
|
||||
row(0, 1), // (0, 1, 1)
|
||||
row(1, 4), // (1, 1, 4)
|
||||
row(2, 7), // (2, 1, 7)
|
||||
row(0, 0), // (0, 0, 0)
|
||||
row(1, 3), // (1, 0, 3)
|
||||
row(2, 6)); // (2, 0, 6)
|
||||
|
||||
// Test ordering of wildcard JSON queries with masked column
|
||||
assertRowsNet(executeNet("SELECT JSON * FROM %s WHERE k IN (0, 1, 2)"),
|
||||
row("{\"k\": 0, \"c\": 0, \"v\": 0}"), // (0, 0, 0)
|
||||
row("{\"k\": 0, \"c\": -1, \"v\": 1}"), // (0, 1, 1)
|
||||
row("{\"k\": 1, \"c\": 0, \"v\": 3}"), // (1, 0, 3)
|
||||
row("{\"k\": 1, \"c\": -1, \"v\": 4}"), // (1, 1, 4)
|
||||
row("{\"k\": 2, \"c\": 0, \"v\": 6}"), // (2, 0, 6)
|
||||
row("{\"k\": 2, \"c\": -1, \"v\": 7}")); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT JSON * FROM %s WHERE k IN (0, 1, 2) ORDER BY c ASC"),
|
||||
row("{\"k\": 0, \"c\": 0, \"v\": 0}"), // (0, 0, 0)
|
||||
row("{\"k\": 1, \"c\": 0, \"v\": 3}"), // (1, 0, 3)
|
||||
row("{\"k\": 2, \"c\": 0, \"v\": 6}"), // (2, 0, 6)
|
||||
row("{\"k\": 0, \"c\": -1, \"v\": 1}"), // (0, 1, 1)
|
||||
row("{\"k\": 1, \"c\": -1, \"v\": 4}"), // (1, 1, 4)
|
||||
row("{\"k\": 2, \"c\": -1, \"v\": 7}")); // (2, 1, 7)
|
||||
assertRowsNet(executeNetWithoutPaging("SELECT JSON * FROM %s WHERE k IN (0, 1, 2) ORDER BY c DESC"),
|
||||
row("{\"k\": 0, \"c\": -1, \"v\": 1}"), // (0, 1, 1)
|
||||
row("{\"k\": 1, \"c\": -1, \"v\": 4}"), // (1, 1, 4)
|
||||
row("{\"k\": 2, \"c\": -1, \"v\": 7}"), // (2, 1, 7)
|
||||
row("{\"k\": 0, \"c\": 0, \"v\": 0}"), // (0, 0, 0)
|
||||
row("{\"k\": 1, \"c\": 0, \"v\": 3}"), // (1, 0, 3)
|
||||
row("{\"k\": 2, \"c\": 0, \"v\": 6}")); // (2, 0, 6)
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnableFlag() throws Throwable
|
||||
{
|
||||
// verify that we cannot create tables with masked columns if DDM is disabled
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(false);
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"CREATE TABLE t (k int PRIMARY KEY, v text MASKED WITH DEFAULT)");
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"CREATE TABLE t (k int MASKED WITH DEFAULT PRIMARY KEY, v text)");
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"CREATE TABLE t (k int, c int MASKED WITH DEFAULT, v text, PRIMARY KEY(k, c))");
|
||||
|
||||
// verify that we cannot mask an existing column if DDM is disabled
|
||||
createTable("CREATE TABLE %s (k int, c int, s int static, r int, PRIMARY KEY(k, c))");
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"ALTER TABLE %s ALTER k MASKED WITH DEFAULT");
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"ALTER TABLE %s ALTER c MASKED WITH DEFAULT");
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"ALTER TABLE %s ALTER s MASKED WITH DEFAULT");
|
||||
assertInvalidThrowMessage(DISABLED_ERROR_MESSAGE,
|
||||
InvalidRequestException.class,
|
||||
"ALTER TABLE %s ALTER r MASKED WITH DEFAULT");
|
||||
|
||||
// enable DDM and add some masked data
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
|
||||
alterTable("ALTER TABLE %s ALTER k MASKED WITH DEFAULT");
|
||||
alterTable("ALTER TABLE %s ALTER c MASKED WITH DEFAULT");
|
||||
alterTable("ALTER TABLE %s ALTER s MASKED WITH DEFAULT");
|
||||
alterTable("ALTER TABLE %s ALTER r MASKED WITH DEFAULT");
|
||||
execute("INSERT INTO %s (k, c, s, r) VALUES (1, 2, 3, 4)");
|
||||
assertRowsNet(executeNet("SELECT * FROM %s"), row(0, 0, 0, 0));
|
||||
|
||||
// verify that column masks are not applied if DDM is disabled
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(false);
|
||||
assertRowsNet(executeNet("SELECT * FROM %s"), row(1, 2, 3, 4));
|
||||
|
||||
// verify that we can drop column masks even if DDM is disabled
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER s DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER r DROP MASKED");
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
|
||||
assertRowsNet(executeNet("SELECT * FROM %s"), row(1, 2, 3, 4));
|
||||
}
|
||||
|
||||
private void assertRowsWithPaging(String query, Object[]... rows)
|
||||
{
|
||||
for (int pageSize : Arrays.asList(1, 2, 3, 4, 5, 100))
|
||||
{
|
||||
assertRowsNet(executeNetWithPaging(query, pageSize), rows);
|
||||
|
||||
for (int limit : Arrays.asList(1, 2, 3, 4, 5, 100))
|
||||
{
|
||||
assertRowsNet(executeNetWithPaging(query + " LIMIT " + limit, pageSize),
|
||||
Arrays.copyOfRange(rows, 0, Math.min(limit, rows.length)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final FunctionFactory NEGATIVE = new FunctionFactory("mask_negative", FunctionParameter.fixed(CQL3Type.Native.INT))
|
||||
{
|
||||
@Override
|
||||
protected NativeFunction doGetOrCreateFunction(List<AbstractType<?>> argTypes, AbstractType<?> receiverType)
|
||||
{
|
||||
return new MaskingFunction(name, argTypes.get(0), argTypes.get(0))
|
||||
{
|
||||
@Override
|
||||
public Masker masker(ByteBuffer... parameters)
|
||||
{
|
||||
return bb -> {
|
||||
if (bb == null)
|
||||
return null;
|
||||
|
||||
Integer value = Int32Type.instance.compose(bb) * -1;
|
||||
return Int32Type.instance.decompose(value);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.UntypedResultSet;
|
||||
import org.apache.cassandra.cql3.functions.ScalarFunction;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.ReversedType;
|
||||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.KeyspaceMetadata;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.SchemaConstants;
|
||||
import org.apache.cassandra.schema.SchemaKeyspace;
|
||||
import org.apache.cassandra.schema.SchemaKeyspaceTables;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.schema.UserFunctions;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* Tests table columns with attached dynamic data masking functions.
|
||||
*/
|
||||
public class ColumnMaskTester extends CQLTester
|
||||
{
|
||||
protected static final String USERNAME = "ddm_user";
|
||||
protected static final String PASSWORD = "ddm_password";
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass()
|
||||
{
|
||||
CQLTester.setUpClass();
|
||||
requireAuthentication();
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() throws Throwable
|
||||
{
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
|
||||
useSuperUser();
|
||||
executeNet(format("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'", USERNAME, PASSWORD));
|
||||
executeNet(format("GRANT ALL ON KEYSPACE %s TO %s", KEYSPACE, USERNAME));
|
||||
executeNet(format("REVOKE UNMASK ON KEYSPACE %s FROM %s", KEYSPACE, USERNAME));
|
||||
useUser(USERNAME, PASSWORD);
|
||||
}
|
||||
|
||||
protected void assertTableColumnsAreNotMasked(String... columns) throws Throwable
|
||||
{
|
||||
for (String column : columns)
|
||||
{
|
||||
assertColumnIsNotMasked(currentTable(), column);
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertViewColumnsAreNotMasked(String... columns) throws Throwable
|
||||
{
|
||||
for (String column : columns)
|
||||
{
|
||||
assertColumnIsNotMasked(currentView(), column);
|
||||
}
|
||||
}
|
||||
|
||||
protected void assertColumnIsNotMasked(String table, String column) throws Throwable
|
||||
{
|
||||
ColumnMask mask = getColumnMask(table, column);
|
||||
assertNull(format("Mask for column '%s'", column), mask);
|
||||
|
||||
assertRows(execute(format("SELECT * FROM %s.%s WHERE keyspace_name = ? AND table_name = ? AND column_name = ?",
|
||||
SchemaConstants.SCHEMA_KEYSPACE_NAME, SchemaKeyspaceTables.COLUMN_MASKS),
|
||||
KEYSPACE, table, column));
|
||||
}
|
||||
|
||||
protected void assertColumnIsMasked(String table,
|
||||
String column,
|
||||
String functionName,
|
||||
List<AbstractType<?>> partialArgumentTypes,
|
||||
List<ByteBuffer> partialArgumentValues) throws Throwable
|
||||
{
|
||||
KeyspaceMetadata keyspaceMetadata = Keyspace.open(KEYSPACE).getMetadata();
|
||||
TableMetadata tableMetadata = keyspaceMetadata.getTableOrViewNullable(table);
|
||||
assertNotNull(tableMetadata);
|
||||
ColumnMetadata columnMetadata = tableMetadata.getColumn(ColumnIdentifier.getInterned(column, false));
|
||||
assertNotNull(columnMetadata);
|
||||
AbstractType<?> columnType = columnMetadata.type;
|
||||
|
||||
// Verify the column mask in the in-memory schema
|
||||
ColumnMask mask = getColumnMask(table, column);
|
||||
assertNotNull(mask);
|
||||
assertThat(mask.partialArgumentTypes()).isEqualTo(columnType.isReversed() && functionName.equals("mask_replace")
|
||||
? Collections.singletonList(ReversedType.getInstance(partialArgumentTypes.get(0)))
|
||||
: partialArgumentTypes);
|
||||
assertThat(mask.partialArgumentValues()).isEqualTo(partialArgumentValues);
|
||||
|
||||
// Verify the function in the column mask
|
||||
ScalarFunction function = mask.function;
|
||||
assertNotNull(function);
|
||||
assertThat(function.name().name).isEqualTo(functionName);
|
||||
assertThat(function.argTypes().get(0).asCQL3Type()).isEqualTo(columnMetadata.type.asCQL3Type());
|
||||
assertThat(function.argTypes().size()).isEqualTo(partialArgumentTypes.size() + 1);
|
||||
|
||||
// Retrieve the persisted column metadata
|
||||
UntypedResultSet columnRows = execute("SELECT * FROM system_schema.columns " +
|
||||
"WHERE keyspace_name = ? AND table_name = ? AND column_name = ?",
|
||||
KEYSPACE, table, column);
|
||||
ColumnMetadata persistedColumn = SchemaKeyspace.createColumnFromRow(columnRows.one(), keyspaceMetadata.types, UserFunctions.none());
|
||||
|
||||
// Verify the column mask in the persisted schema
|
||||
ColumnMask savedMask = persistedColumn.getMask();
|
||||
assertNotNull(savedMask);
|
||||
assertThat(mask).isEqualTo(savedMask);
|
||||
assertThat(mask.function.argTypes()).isEqualTo(savedMask.function.argTypes());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ColumnMask getColumnMask(String table, String column)
|
||||
{
|
||||
TableMetadata tableMetadata = Schema.instance.getTableMetadata(KEYSPACE, table);
|
||||
assertNotNull(tableMetadata);
|
||||
ColumnMetadata columnMetadata = tableMetadata.getColumn(ColumnIdentifier.getInterned(column, false));
|
||||
|
||||
if (columnMetadata == null)
|
||||
fail(format("Unknown column '%s'", column));
|
||||
|
||||
return columnMetadata.getMask();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import org.apache.cassandra.cql3.CQL3Type;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.ASCII;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.BIGINT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.BLOB;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.INT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.TEXT;
|
||||
import static org.apache.cassandra.cql3.CQL3Type.Native.VARINT;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* {@link ColumnMaskTester} for masks using functions that might return values with a type different to the type of the
|
||||
* masked column. Those queries should fail.
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ColumnMaskWithTypeAlteringFunctionTest extends ColumnMaskTester
|
||||
{
|
||||
/** The column mask as expressed in CQL statements right after the {@code MASKED WITH} keywords. */
|
||||
@Parameterized.Parameter
|
||||
public String mask;
|
||||
|
||||
/** The type of the masked column. */
|
||||
@Parameterized.Parameter(1)
|
||||
public CQL3Type.Native type;
|
||||
|
||||
/** The type returned by the tested masking function. */
|
||||
@Parameterized.Parameter(2)
|
||||
public CQL3Type returnedType;
|
||||
|
||||
private boolean shouldSucceed;
|
||||
private String errorMessage;
|
||||
|
||||
@Parameterized.Parameters(name = "mask={0} type={1}")
|
||||
public static Collection<Object[]> options()
|
||||
{
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "mask_default()", INT, INT },
|
||||
{ "mask_default()", TEXT, TEXT },
|
||||
{ "mask_null()", INT, INT },
|
||||
{ "mask_null()", TEXT, TEXT },
|
||||
{ "mask_hash()", INT, BLOB },
|
||||
{ "mask_hash('SHA-512')", INT, BLOB },
|
||||
{ "mask_inner(1,2)", TEXT, TEXT },
|
||||
{ "mask_outer(1,2)", TEXT, TEXT },
|
||||
{ "mask_replace(0)", INT, INT },
|
||||
{ "mask_replace(0)", BIGINT, BIGINT },
|
||||
{ "mask_replace(0)", VARINT, VARINT },
|
||||
{ "mask_replace('redacted')", ASCII, ASCII },
|
||||
{ "mask_replace('redacted')", TEXT, TEXT } });
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setupExpectedResults()
|
||||
{
|
||||
shouldSucceed = returnedType == type;
|
||||
errorMessage = shouldSucceed ? null : format("Masking function %s return type is %s.", mask, returnedType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTypeAlteringFunctionOnCreateTable()
|
||||
{
|
||||
testOnCreateTable("CREATE TABLE %s.%s (k int PRIMARY KEY, v %s MASKED WITH %s)");
|
||||
testOnCreateTable("CREATE TABLE %s.%s (k %s MASKED WITH %s PRIMARY KEY, v int)");
|
||||
testOnCreateTable("CREATE TABLE %s.%s (k1 int, k2 %s MASKED WITH %s, PRIMARY KEY((k1, k2)))");
|
||||
testOnCreateTable("CREATE TABLE %s.%s (k int, c %s MASKED WITH %s, PRIMARY KEY(k, c)) WITH CLUSTERING ORDER BY (c ASC)");
|
||||
testOnCreateTable("CREATE TABLE %s.%s (k int, c %s MASKED WITH %s, PRIMARY KEY(k, c)) WITH CLUSTERING ORDER BY (c DESC)");
|
||||
testOnCreateTable("CREATE TABLE %s.%s (k int, c int, s %s STATIC MASKED WITH %s, PRIMARY KEY(k, c))");
|
||||
}
|
||||
|
||||
private void testOnCreateTable(String query)
|
||||
{
|
||||
String formattedQuery = format(query, KEYSPACE, createTableName(), type, mask);
|
||||
if (shouldSucceed)
|
||||
createTable(formattedQuery);
|
||||
else
|
||||
assertThatThrownBy(() -> execute(formattedQuery)).hasMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTypeAlteringFunctionOnMaskColumn()
|
||||
{
|
||||
testOnAlterColumn("CREATE TABLE %s.%s (k int PRIMARY KEY, v %s)",
|
||||
"ALTER TABLE %s.%s ALTER v MASKED WITH %s");
|
||||
testOnAlterColumn("CREATE TABLE %s.%s (k %s PRIMARY KEY, v int)",
|
||||
"ALTER TABLE %s.%s ALTER k MASKED WITH %s");
|
||||
testOnAlterColumn("CREATE TABLE %s.%s (k1 int, k2 %s, PRIMARY KEY((k1, k2)))",
|
||||
"ALTER TABLE %s.%s ALTER k2 MASKED WITH %s");
|
||||
testOnAlterColumn("CREATE TABLE %s.%s (k int, c %s, PRIMARY KEY(k, c)) WITH CLUSTERING ORDER BY (c ASC)",
|
||||
"ALTER TABLE %s.%s ALTER c MASKED WITH %s");
|
||||
testOnAlterColumn("CREATE TABLE %s.%s (k int, c %s, PRIMARY KEY(k, c)) WITH CLUSTERING ORDER BY (c DESC)",
|
||||
"ALTER TABLE %s.%s ALTER c MASKED WITH %s");
|
||||
testOnAlterColumn("CREATE TABLE %s.%s (k int, c int, s %s STATIC, PRIMARY KEY(k, c))",
|
||||
"ALTER TABLE %s.%s ALTER s MASKED WITH %s");
|
||||
}
|
||||
|
||||
private void testOnAlterColumn(String createQuery, String alterQuery)
|
||||
{
|
||||
String table = createTableName();
|
||||
createTable(format(createQuery, KEYSPACE, table, type));
|
||||
|
||||
String formattedQuery = format(alterQuery, KEYSPACE, table, mask);
|
||||
if (shouldSucceed)
|
||||
alterTable(formattedQuery);
|
||||
else
|
||||
assertThatThrownBy(() -> execute(formattedQuery)).hasMessageContaining(errorMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTypeAlteringFunctionOnAddColumn()
|
||||
{
|
||||
String table = createTable(format("CREATE TABLE %%s (k int PRIMARY KEY, v %s)", type));
|
||||
String query = format("ALTER TABLE %s.%s ADD n %s MASKED WITH %s", KEYSPACE, table, type, mask);
|
||||
if (shouldSucceed)
|
||||
alterTable(query);
|
||||
else
|
||||
assertThatThrownBy(() -> execute(query)).hasMessageContaining(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* 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;'");
|
||||
}
|
||||
}
|
||||
|
|
@ -192,7 +192,7 @@ public abstract class MaskingFunctionTester extends CQLTester
|
|||
* Tests the native masking function for the specified column type and values on all possible types of column.
|
||||
* That is, when the column is part of the primary key, or a regular column, or a static column.
|
||||
*
|
||||
* @param type the type of the tested column
|
||||
* @param type the type of the tested column
|
||||
* @param values the values of the tested column
|
||||
*/
|
||||
private void testMaskingOnAllColumns(CQL3Type type, Object... values) throws Throwable
|
||||
|
|
@ -222,7 +222,7 @@ public abstract class MaskingFunctionTester extends CQLTester
|
|||
* Tests the native masking function for the specified column type and values when the column isn't part of the
|
||||
* primary key. That is, when the column is either a regular column or a static column.
|
||||
*
|
||||
* @param type the type of the tested column
|
||||
* @param type the type of the tested column
|
||||
* @param values the values of the tested column
|
||||
*/
|
||||
private void testMaskingOnNotKeyColumns(CQL3Type type, Object... values) throws Throwable
|
||||
|
|
@ -265,8 +265,8 @@ public abstract class MaskingFunctionTester extends CQLTester
|
|||
* Tests the native masking function for the specified column type and value.
|
||||
* This assumes that the table is already created.
|
||||
*
|
||||
* @param name the name of the tested column
|
||||
* @param type the type of the tested column
|
||||
* @param name the name of the tested column
|
||||
* @param type the type of the tested column
|
||||
* @param value the value of the tested column
|
||||
*/
|
||||
protected abstract void testMaskingOnColumn(String name, CQL3Type type, Object value) throws Throwable;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
* 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.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
import org.awaitility.core.ThrowingRunnable;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
/**
|
||||
* Tests the {@link org.apache.cassandra.auth.Permission#SELECT_MASKED} permission.
|
||||
*/
|
||||
public class SelectMaskedPermissionTest extends CQLTester
|
||||
{
|
||||
private static final Object[] CLEAR_ROW = row(7, 7, 7, 7);
|
||||
|
||||
private static final String USER = "ddm_user"; // user that will have their permissions changed
|
||||
private static final String PASSWORD = "ddm_password";
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass()
|
||||
{
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
|
||||
DatabaseDescriptor.setPermissionsValidity(0);
|
||||
DatabaseDescriptor.setRolesValidity(0);
|
||||
CQLTester.setUpClass();
|
||||
requireAuthentication();
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() throws Throwable
|
||||
{
|
||||
useSuperUser();
|
||||
|
||||
createTable("CREATE TABLE %s (k int, c int, s int static, v int, PRIMARY KEY (k, c))");
|
||||
executeNet("INSERT INTO %s(k, c, s, v) VALUES (?, ?, ?, ?)", CLEAR_ROW);
|
||||
|
||||
executeNet(format("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'", USER, PASSWORD));
|
||||
executeNet(format("GRANT SELECT ON ALL KEYSPACES TO %s", USER));
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() throws Throwable
|
||||
{
|
||||
useSuperUser();
|
||||
executeNet("DROP USER IF EXISTS " + USER);
|
||||
alterTable("ALTER TABLE %s ALTER k DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER c DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER s DROP MASKED");
|
||||
alterTable("ALTER TABLE %s ALTER v DROP MASKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionKeyColumn() throws Throwable
|
||||
{
|
||||
alterTable("ALTER TABLE %s ALTER k MASKED WITH DEFAULT");
|
||||
Object[] maskedRow = row(0, 7, 7, 7);
|
||||
|
||||
// test queries with default permissions (no UNMASK nor SELECT_MASKED)
|
||||
testPartitionKeyColumnWithDefaultPermissions(maskedRow);
|
||||
|
||||
// test queries with only SELECT_MASKED permission
|
||||
executeNet(format("GRANT SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testPartitionKeyColumnWithOnlySelectMasked(maskedRow);
|
||||
|
||||
// test queries with only UNMASK permission (which includes SELECT_MASKED)
|
||||
executeNet(format("REVOKE SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
executeNet(format("GRANT UNMASK ON ALL KEYSPACES TO %s", USER));
|
||||
testPartitionKeyColumnWithUnmask();
|
||||
|
||||
// test queries with both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("GRANT UNMASK, SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testPartitionKeyColumnWithUnmask();
|
||||
|
||||
// test queries again without both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("REVOKE UNMASK, SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
testPartitionKeyColumnWithDefaultPermissions(maskedRow);
|
||||
}
|
||||
|
||||
private void testPartitionKeyColumnWithDefaultPermissions(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE k = 7", "[k]");
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE k >= 7 ALLOW FILTERING", "[k]");
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", "[k]");
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE k = 7 AND v = 7 ALLOW FILTERING", "[k]");
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", "[k]");
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE token(k) >= token(7)", "[k]");
|
||||
});
|
||||
}
|
||||
|
||||
private void testPartitionKeyColumnWithOnlySelectMasked(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k >= 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND v = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) >= token(7)", maskedRow);
|
||||
});
|
||||
}
|
||||
|
||||
private void testPartitionKeyColumnWithUnmask() throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k >= 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND v = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) >= token(7)", CLEAR_ROW);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClusteringKeyColumn() throws Throwable
|
||||
{
|
||||
alterTable("ALTER TABLE %s ALTER c MASKED WITH DEFAULT");
|
||||
Object[] maskedRow = row(7, 0, 7, 7);
|
||||
|
||||
// test queries with default permissions (no UNMASK nor SELECT_MASKED)
|
||||
testClusteringKeyColumnWithDefaultPermissions(maskedRow);
|
||||
|
||||
// test queries with only SELECT_MASKED permission
|
||||
executeNet(format("GRANT SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testClusteringKeyColumnWithOnlySelectMasked(maskedRow);
|
||||
|
||||
// test queries with only UNMASK permission (which includes SELECT_MASKED)
|
||||
executeNet(format("REVOKE SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
executeNet(format("GRANT UNMASK ON ALL KEYSPACES TO %s", USER));
|
||||
testClusteringKeyColumnWithUnmask();
|
||||
|
||||
// test queries with both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("GRANT UNMASK, SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testClusteringKeyColumnWithUnmask();
|
||||
|
||||
// test queries again without both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("REVOKE UNMASK, SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
testClusteringKeyColumnWithDefaultPermissions(maskedRow);
|
||||
}
|
||||
|
||||
private void testClusteringKeyColumnWithDefaultPermissions(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", "[c]");
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", "[c]");
|
||||
});
|
||||
}
|
||||
|
||||
private void testClusteringKeyColumnWithOnlySelectMasked(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", maskedRow);
|
||||
});
|
||||
}
|
||||
|
||||
private void testClusteringKeyColumnWithUnmask() throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", CLEAR_ROW);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticColumn() throws Throwable
|
||||
{
|
||||
alterTable("ALTER TABLE %s ALTER s MASKED WITH DEFAULT");
|
||||
Object[] maskedRow = row(7, 7, 0, 7);
|
||||
|
||||
// test queries with default permissions (no UNMASK nor SELECT_MASKED)
|
||||
testStaticColumnWithDefaultPermissions(maskedRow);
|
||||
|
||||
// test queries with only SELECT_MASKED permission
|
||||
executeNet(format("GRANT SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testStaticColumnWithOnlySelectMasked(maskedRow);
|
||||
|
||||
// test queries with only UNMASK permission (which includes SELECT_MASKED)
|
||||
executeNet(format("REVOKE SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
executeNet(format("GRANT UNMASK ON ALL KEYSPACES TO %s", USER));
|
||||
testStaticColumnWithUnmask();
|
||||
|
||||
// test queries with both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("GRANT UNMASK, SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testStaticColumnWithUnmask();
|
||||
|
||||
// test queries again without both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("REVOKE UNMASK, SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
testStaticColumnWithDefaultPermissions(maskedRow);
|
||||
}
|
||||
|
||||
private void testStaticColumnWithDefaultPermissions(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", "[s]");
|
||||
});
|
||||
}
|
||||
|
||||
private void testStaticColumnWithOnlySelectMasked(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
});
|
||||
}
|
||||
|
||||
private void testStaticColumnWithUnmask() throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCollections() throws Throwable
|
||||
{
|
||||
alterTable("ALTER TABLE %s ALTER v MASKED WITH DEFAULT");
|
||||
Object[] maskedRow = row(7, 7, 7, 0);
|
||||
|
||||
// test queries with default permissions (no UNMASK nor SELECT_MASKED)
|
||||
testCollectionsWithDefaultPermissions(maskedRow);
|
||||
|
||||
// test queries with only SELECT_MASKED permission
|
||||
executeNet(format("GRANT SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testCollectionsWithOnlySelectMasked(maskedRow);
|
||||
|
||||
// test queries with only UNMASK permission (which includes SELECT_MASKED)
|
||||
executeNet(format("REVOKE SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
executeNet(format("GRANT UNMASK ON ALL KEYSPACES TO %s", USER));
|
||||
testCollectionsWithUnmask();
|
||||
|
||||
// test queries with both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("GRANT UNMASK, SELECT_MASKED ON ALL KEYSPACES TO %s", USER));
|
||||
testCollectionsWithUnmask();
|
||||
|
||||
// test queries again without both UNMASK and SELECT_MASKED permissions
|
||||
executeNet(format("REVOKE UNMASK, SELECT_MASKED ON ALL KEYSPACES FROM %s", USER));
|
||||
testCollectionsWithDefaultPermissions(maskedRow);
|
||||
}
|
||||
|
||||
private void testCollectionsWithDefaultPermissions(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertUnauthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", "[v]");
|
||||
});
|
||||
}
|
||||
|
||||
private void testCollectionsWithOnlySelectMasked(Object[] maskedRow) throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", maskedRow);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", maskedRow);
|
||||
});
|
||||
}
|
||||
|
||||
private void testCollectionsWithUnmask() throws Throwable
|
||||
{
|
||||
assertWithUser(() -> {
|
||||
assertAuthorizedQuery("SELECT * FROM %s", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE token(k) = token(7)", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE k = 7 AND c = 7", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE c = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE s = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
assertAuthorizedQuery("SELECT * FROM %s WHERE v = 7 ALLOW FILTERING", CLEAR_ROW);
|
||||
});
|
||||
}
|
||||
|
||||
private void assertAuthorizedQuery(String query, Object[]... rows) throws Throwable
|
||||
{
|
||||
assertRowsNet(executeNet(query), rows);
|
||||
}
|
||||
|
||||
private void assertUnauthorizedQuery(String query, String unauthorizedColumns) throws Throwable
|
||||
{
|
||||
assertInvalidMessageNet(format("User %s has no UNMASK nor SELECT_MASKED permission on table %s.%s, " +
|
||||
"cannot query masked columns %s",
|
||||
USER, KEYSPACE, currentTable(), unauthorizedColumns), query);
|
||||
}
|
||||
|
||||
private void assertWithUser(ThrowingRunnable assertion) throws Throwable
|
||||
{
|
||||
useUser(USER, PASSWORD);
|
||||
assertion.run();
|
||||
useSuperUser();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* 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 java.util.Arrays;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLTester;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
/**
|
||||
* Tests the {@link org.apache.cassandra.auth.Permission#UNMASK} permission.
|
||||
* <p>
|
||||
* The permission is tested for a regular user with the {@code UNMASK} permissions on different resources,
|
||||
* while also verifying the absence of side effects on other ordinary users, superusers and internal queries.
|
||||
*/
|
||||
public class UnmaskPermissionTest extends CQLTester
|
||||
{
|
||||
private static final String CREATE_KEYSPACE = "CREATE KEYSPACE IF NOT EXISTS %s WITH replication = " +
|
||||
"{'class': 'SimpleStrategy', 'replication_factor': '1'}";
|
||||
private static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS %s.%s " +
|
||||
"(k int, c int, v text MASKED WITH mask_replace('redacted'), " +
|
||||
"PRIMARY KEY (k, c))";
|
||||
private static final String INSERT = "INSERT INTO %s.%s (k, c, v) VALUES (?, ?, ?)";
|
||||
private static final String SELECT_WILDCARD = "SELECT * FROM %s.%s";
|
||||
private static final String SELECT_COLUMNS = "SELECT k, c, v FROM %s.%s";
|
||||
|
||||
private static final Object[] CLEAR_ROW = row(0, 0, "sensitive");
|
||||
private static final Object[] MASKED_ROW = row(0, 0, "redacted");
|
||||
|
||||
private static final String KEYSPACE_1 = "mask_keyspace_1";
|
||||
private static final String KEYSPACE_2 = "mask_keyspace_2";
|
||||
private static final String TABLE_1 = "mask_table_1";
|
||||
private static final String TABLE_2 = "mask_table_2";
|
||||
|
||||
private static final String USER = "ddm_user"; // user that will have their permissions changed
|
||||
private static final String OTHER_USER = "ddm_ordinary_user"; // user that won't have their permissions altered
|
||||
private static final String PASSWORD = "ddm_password";
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass()
|
||||
{
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
|
||||
DatabaseDescriptor.setPermissionsValidity(0);
|
||||
DatabaseDescriptor.setRolesValidity(0);
|
||||
CQLTester.setUpClass();
|
||||
requireAuthentication();
|
||||
requireNetwork();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() throws Throwable
|
||||
{
|
||||
useSuperUser();
|
||||
|
||||
schemaChange(format(CREATE_KEYSPACE, KEYSPACE_1));
|
||||
schemaChange(format(CREATE_KEYSPACE, KEYSPACE_2));
|
||||
|
||||
createTable(format(CREATE_TABLE, KEYSPACE_1, TABLE_1));
|
||||
createTable(format(CREATE_TABLE, KEYSPACE_1, TABLE_2));
|
||||
createTable(format(CREATE_TABLE, KEYSPACE_2, TABLE_1));
|
||||
|
||||
execute(format(INSERT, KEYSPACE_1, TABLE_1), CLEAR_ROW);
|
||||
execute(format(INSERT, KEYSPACE_1, TABLE_2), CLEAR_ROW);
|
||||
execute(format(INSERT, KEYSPACE_2, TABLE_1), CLEAR_ROW);
|
||||
|
||||
for (String user : Arrays.asList(USER, OTHER_USER))
|
||||
{
|
||||
executeNet(format("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'", user, PASSWORD));
|
||||
executeNet(format("GRANT SELECT ON ALL KEYSPACES TO %s", user));
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() throws Throwable
|
||||
{
|
||||
useSuperUser();
|
||||
executeNet("DROP USER IF EXISTS " + USER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnmaskDefaults() throws Throwable
|
||||
{
|
||||
// ordinary user without changed permissions should see masked data
|
||||
useUser(OTHER_USER, PASSWORD);
|
||||
assertMasked(KEYSPACE_1, TABLE_1);
|
||||
assertMasked(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
|
||||
// super user should see unmasked data
|
||||
useSuperUser();
|
||||
assertClear(KEYSPACE_1, TABLE_1);
|
||||
assertClear(KEYSPACE_1, TABLE_2);
|
||||
assertClear(KEYSPACE_2, TABLE_1);
|
||||
|
||||
// internal user should see unmasked data
|
||||
assertClearInternal(KEYSPACE_1, TABLE_1);
|
||||
assertClearInternal(KEYSPACE_1, TABLE_2);
|
||||
assertClearInternal(KEYSPACE_2, TABLE_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnmaskOnAllKeyspaces() throws Throwable
|
||||
{
|
||||
assertPermissions(format("GRANT UNMASK ON ALL KEYSPACES TO %s", USER), () -> {
|
||||
assertClear(KEYSPACE_1, TABLE_1);
|
||||
assertClear(KEYSPACE_1, TABLE_2);
|
||||
assertClear(KEYSPACE_2, TABLE_1);
|
||||
});
|
||||
|
||||
assertPermissions(format("REVOKE UNMASK ON ALL KEYSPACES FROM %s", USER), () -> {
|
||||
assertMasked(KEYSPACE_1, TABLE_1);
|
||||
assertMasked(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnmaskOnKeyspace() throws Throwable
|
||||
{
|
||||
assertPermissions(format("GRANT UNMASK ON KEYSPACE %s TO %s", KEYSPACE_1, USER), () -> {
|
||||
assertClear(KEYSPACE_1, TABLE_1);
|
||||
assertClear(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
});
|
||||
|
||||
assertPermissions(format("REVOKE UNMASK ON KEYSPACE %s FROM %s", KEYSPACE_1, USER), () -> {
|
||||
assertMasked(KEYSPACE_1, TABLE_1);
|
||||
assertMasked(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnmaskOnTable() throws Throwable
|
||||
{
|
||||
assertPermissions(format("GRANT UNMASK ON TABLE %s.%s TO %s", KEYSPACE_1, TABLE_1, USER), () -> {
|
||||
assertClear(KEYSPACE_1, TABLE_1);
|
||||
assertMasked(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
});
|
||||
|
||||
assertPermissions(format("REVOKE UNMASK ON TABLE %s.%s FROM %s", KEYSPACE_1, TABLE_1, USER), () -> {
|
||||
assertMasked(KEYSPACE_1, TABLE_1);
|
||||
assertMasked(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
});
|
||||
}
|
||||
|
||||
private void assertPermissions(String alterPermissionsQuery, ThrowingRunnable assertion) throws Throwable
|
||||
{
|
||||
// alter permissions as superuser
|
||||
useSuperUser();
|
||||
executeNet(alterPermissionsQuery);
|
||||
|
||||
// verify the tested user permissions
|
||||
useUser(USER, PASSWORD);
|
||||
assertion.run();
|
||||
|
||||
// the ordinary user without modified permissions should keep seeing masked data
|
||||
useUser(OTHER_USER, PASSWORD);
|
||||
assertMasked(KEYSPACE_1, TABLE_1);
|
||||
assertMasked(KEYSPACE_1, TABLE_2);
|
||||
assertMasked(KEYSPACE_2, TABLE_1);
|
||||
|
||||
// super user should keep seeing unmasked data
|
||||
useSuperUser();
|
||||
assertClear(KEYSPACE_1, TABLE_1);
|
||||
assertClear(KEYSPACE_1, TABLE_2);
|
||||
assertClear(KEYSPACE_2, TABLE_1);
|
||||
|
||||
// internal user should keep seeing unmasked data
|
||||
assertClearInternal(KEYSPACE_1, TABLE_1);
|
||||
assertClearInternal(KEYSPACE_1, TABLE_2);
|
||||
assertClearInternal(KEYSPACE_2, TABLE_1);
|
||||
}
|
||||
|
||||
private void assertMasked(String keyspace, String table) throws Throwable
|
||||
{
|
||||
assertRowsNet(executeNet(format(SELECT_WILDCARD, keyspace, table)), MASKED_ROW);
|
||||
assertRowsNet(executeNet(format(SELECT_COLUMNS, keyspace, table)), MASKED_ROW);
|
||||
}
|
||||
|
||||
private void assertClear(String keyspace, String table) throws Throwable
|
||||
{
|
||||
assertRowsNet(executeNet(format(SELECT_WILDCARD, keyspace, table)), CLEAR_ROW);
|
||||
assertRowsNet(executeNet(format(SELECT_COLUMNS, keyspace, table)), CLEAR_ROW);
|
||||
}
|
||||
|
||||
private void assertClearInternal(String keyspace, String table) throws Throwable
|
||||
{
|
||||
assertRows(execute(format(SELECT_WILDCARD, keyspace, table)), CLEAR_ROW);
|
||||
assertRows(execute(format(SELECT_COLUMNS, keyspace, table)), CLEAR_ROW);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ThrowingRunnable
|
||||
{
|
||||
void run() throws Throwable;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,12 +39,14 @@ import org.apache.cassandra.locator.InetAddressAndPort;
|
|||
import org.apache.cassandra.locator.TokenMetadata;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.TableId;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.apache.cassandra.schema.SchemaConstants.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
|
|
@ -757,6 +759,56 @@ public class DescribeStatementTest extends CQLTester
|
|||
row(KEYSPACE_PER_TEST, "index", indexWithOptions, expectedIndexStmtWithOptions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDescribeTableWithColumnMasks() throws Throwable
|
||||
{
|
||||
requireNetwork();
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(true);
|
||||
|
||||
String table = createTable(KEYSPACE_PER_TEST,
|
||||
"CREATE TABLE %s (" +
|
||||
" pk1 text, " +
|
||||
" pk2 int MASKED WITH DEFAULT, " +
|
||||
" ck1 int, " +
|
||||
" ck2 int MASKED WITH mask_default()," +
|
||||
" s1 decimal static, " +
|
||||
" s2 decimal static MASKED WITH mask_null(), " +
|
||||
" v1 text, " +
|
||||
" v2 text MASKED WITH mask_inner(1, null), " +
|
||||
"PRIMARY KEY ((pk1, pk2), ck1, ck2 ))");
|
||||
|
||||
TableMetadata tableMetadata = Schema.instance.getTableMetadata(KEYSPACE_PER_TEST, table);
|
||||
assertNotNull(tableMetadata);
|
||||
|
||||
String tableCreateStatement = "CREATE TABLE " + KEYSPACE_PER_TEST + "." + table + " (\n" +
|
||||
" pk1 text,\n" +
|
||||
" pk2 int MASKED WITH system.mask_default(),\n" +
|
||||
" ck1 int,\n" +
|
||||
" ck2 int MASKED WITH system.mask_default(),\n" +
|
||||
" s1 decimal static,\n" +
|
||||
" s2 decimal static MASKED WITH system.mask_null(),\n" +
|
||||
" v1 text,\n" +
|
||||
" v2 text MASKED WITH system.mask_inner(1, null),\n" +
|
||||
" PRIMARY KEY ((pk1, pk2), ck1, ck2)\n" +
|
||||
") WITH ID = " + tableMetadata.id + "\n" +
|
||||
" AND CLUSTERING ORDER BY (ck1 ASC, ck2 ASC)\n" +
|
||||
" AND " + tableParametersCql();
|
||||
|
||||
assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + table + " WITH INTERNALS"),
|
||||
row(KEYSPACE_PER_TEST,
|
||||
"table",
|
||||
table,
|
||||
tableCreateStatement));
|
||||
|
||||
// masks should be listed even if DDM is disabled
|
||||
DatabaseDescriptor.setDynamicDataMaskingEnabled(false);
|
||||
assertRowsNet(executeDescribeNet("DESCRIBE TABLE " + KEYSPACE_PER_TEST + "." + table + " WITH INTERNALS"),
|
||||
row(KEYSPACE_PER_TEST,
|
||||
"table",
|
||||
table,
|
||||
tableCreateStatement));
|
||||
}
|
||||
|
||||
private static String allTypesTable()
|
||||
{
|
||||
return "CREATE TABLE test.has_all_types (\n" +
|
||||
|
|
@ -918,9 +970,4 @@ public class DescribeStatementTest extends CQLTester
|
|||
executeNet(v, "USE " + useKs);
|
||||
return v;
|
||||
}
|
||||
|
||||
private static Object[][] rows(Object[]... rows)
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ public class CellTest
|
|||
ColumnIdentifier.getInterned(name, false),
|
||||
type,
|
||||
ColumnMetadata.NO_POSITION,
|
||||
ColumnMetadata.Kind.REGULAR);
|
||||
ColumnMetadata.Kind.REGULAR,
|
||||
null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -496,7 +496,7 @@ public class ColumnsTest
|
|||
|
||||
private static ColumnMetadata def(String name, AbstractType<?> type, ColumnMetadata.Kind kind)
|
||||
{
|
||||
return new ColumnMetadata(TABLE_METADATA, bytes(name), type, ColumnMetadata.NO_POSITION, kind);
|
||||
return new ColumnMetadata(TABLE_METADATA, bytes(name), type, ColumnMetadata.NO_POSITION, kind, null);
|
||||
}
|
||||
|
||||
private static TableMetadata mock(Columns columns)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue