Skip to content

Commit c18cb33

Browse files
authored
fix(query): prevent modification of columns with security policies (#18896)
* fix(query): prevent modification of columns with security policies This patch addresses two critical security policy enforcement issues: Bug-1: Prevent dropping or modifying columns with attached policies Columns that have masking policies or serve as row access policy parameters must not be dropped or modified, as this would break security policy enforcement. Added validation in: - interpreter_table_drop_column: Blocks DROP COLUMN operations - interpreter_table_modify_column: Blocks data type changes Bug-2: Prevent duplicate policy assignment on columns A column cannot have multiple security policies attached simultaneously. Added checks in: - interpreter_table_modify_column: Validates before SET MASKING POLICY - interpreter_table_row_access_add: Validates before SET ROW ACCESS POLICY Implementation: - Created check_column_has_policy() utility in interpreters/util.rs - Validates against both masking and row access policies - Returns descriptive error messages to prevent security violations This ensures policy integrity and prevents accidental security configuration corruption during table schema alterations. * optimize
1 parent 69269a0 commit c18cb33

File tree

5 files changed

+68
-9
lines changed

5 files changed

+68
-9
lines changed

โ€Žsrc/meta/app/src/schema/table/ops.rsโ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::sync::Arc;
1818

1919
use databend_common_exception::ErrorCode;
2020
use databend_common_exception::Result;
21+
use databend_common_expression::ColumnId;
2122
use databend_common_expression::FieldIndex;
2223
use databend_common_expression::TableField;
2324

@@ -83,6 +84,16 @@ impl TableMeta {
8384
};
8485
Ok(())
8586
}
87+
88+
/// Check if a column reference a security policy.
89+
pub fn is_column_reference_policy(&self, column_id: &ColumnId) -> bool {
90+
self.column_mask_policy_columns_ids.contains_key(column_id)
91+
|| self
92+
.row_access_policy_columns_ids
93+
.as_ref()
94+
.map(|policy| policy.columns_ids.contains(column_id))
95+
.unwrap_or(false)
96+
}
8697
}
8798

8899
#[cfg(test)]

โ€Žsrc/query/service/src/interpreters/interpreter_table_drop_column.rsโ€Ž

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,12 @@ impl Interpreter for DropTableColumnInterpreter {
8585

8686
let table_schema = table_info.schema();
8787
let field = table_schema.field_with_name(self.plan.column.as_str())?;
88-
if let Some(p) = &table_info.meta.row_access_policy_columns_ids {
89-
if p.columns_ids.contains(&field.column_id) {
90-
return Err(ErrorCode::AlterTableError(format!(
91-
"Cannot drop column '{}' which is associated with a Row access policy",
92-
self.plan.column.as_str()
93-
)));
94-
}
88+
89+
if table_info.meta.is_column_reference_policy(&field.column_id) {
90+
return Err(ErrorCode::AlterTableError(format!(
91+
"Cannot drop column '{}' which is associated with a security policy",
92+
self.plan.column.as_str()
93+
)));
9594
}
9695
if field.computed_expr().is_none() {
9796
let mut schema: DataSchema = table_info.schema().into();

โ€Žsrc/query/service/src/interpreters/interpreter_table_modify_column.rsโ€Ž

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ impl ModifyTableColumnInterpreter {
146146
ErrorCode::UnknownColumn(format!("Cannot find column {}", column))
147147
})?;
148148

149+
if table_info
150+
.meta
151+
.is_column_reference_policy(&data_field.column_id)
152+
{
153+
return Err(ErrorCode::AlterTableError(format!(
154+
"Column '{}' is already attached to a security policy. A column cannot be attached to multiple security policies",
155+
data_field.name
156+
)));
157+
}
158+
149159
let column_type = data_field.data_type();
150160
if policy_data_type != column_type.remove_nullable() {
151161
return Err(ErrorCode::UnmatchColumnDataType(format!(
@@ -241,6 +251,16 @@ impl ModifyTableColumnInterpreter {
241251
let mut modify_comment = false;
242252
for (field, comment) in field_and_comments {
243253
if let Some((i, old_field)) = schema.column_with_name(&field.name) {
254+
if table_info
255+
.meta
256+
.is_column_reference_policy(&old_field.column_id)
257+
{
258+
return Err(ErrorCode::AlterTableError(format!(
259+
"Cannot modify column '{}' which is associated with a security policy",
260+
old_field.name
261+
)));
262+
}
263+
244264
if old_field.data_type != field.data_type {
245265
// If the column is defined in bloom index columns,
246266
// check whether the data type is supported for bloom index.

โ€Žsrc/query/service/src/interpreters/interpreter_table_row_access_add.rsโ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ impl Interpreter for AddTableRowAccessPolicyInterpreter {
120120

121121
for (column, policy_data_type) in columns.iter().zip(policy_data_types.into_iter()) {
122122
if let Some((_, data_field)) = schema.column_with_name(column) {
123+
if table
124+
.get_table_info()
125+
.meta
126+
.is_column_reference_policy(&data_field.column_id)
127+
{
128+
return Err(ErrorCode::AlterTableError(format!(
129+
"Column '{}' is already attached to a security policy. A column cannot be attached to multiple security policies",
130+
data_field.name
131+
)));
132+
}
123133
let column_type = data_field.data_type();
124134
if policy_data_type != column_type.remove_nullable() {
125135
return Err(ErrorCode::UnmatchColumnDataType(format!(

โ€Žtests/sqllogictests/suites/ee/05_ee_ddl/05_0004_ddl_security_policy.testโ€Ž

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ select id from t_1;
136136
----
137137
2
138138

139-
statement error
139+
statement error 1132
140+
alter table t_1 modify column cc string not null;
141+
142+
statement error 1132
140143
alter table t_1 drop column cc;
141144

142145
statement ok
@@ -301,6 +304,12 @@ CREATE MASKING POLICY maskc AS (val int) RETURNS int -> CASE WHEN current_role()
301304
statement ok
302305
alter table data_mask_test modify column b set masking policy maskb;
303306

307+
statement error 1132
308+
alter table data_mask_test modify column b string not null;
309+
310+
statement error 1132
311+
alter table data_mask_test drop column b;
312+
304313
query ITT
305314
select * from data_mask_test;
306315
----
@@ -309,6 +318,9 @@ select * from data_mask_test;
309318
statement ok
310319
alter table data_mask_test modify column a set masking policy maska;
311320

321+
statement error 1132
322+
alter table data_mask_test modify column a set masking policy maskc;
323+
312324
query ITT
313325
select * from data_mask_test;
314326
----
@@ -394,6 +406,10 @@ COMMENT = 'Mask SSN conditionally based on role';
394406
statement ok
395407
alter table employees modify column ssn set masking policy mask_ssn_conditional using (ssn, role);
396408

409+
## Test 2: Apply policy with USING clause - Column 'ssn' is already attached to a security policy.
410+
statement error 1132
411+
alter table employees modify column role set masking policy mask_ssn_conditional using (role, ssn);
412+
397413
## Query should mask SSN for all rows since current_role() won't match
398414
query ITTIT
399415
select id, name, ssn, salary, department from employees order by id;
@@ -464,7 +480,7 @@ drop masking policy if exists mask_int_policy;
464480
statement ok
465481
CREATE MASKING POLICY mask_int_policy AS (val STRING, num INT) RETURNS STRING -> val;
466482

467-
## Should fail - department is STRING, not INT
483+
## Should fail - Column 'department' is already attached to a security policy. A column cannot be attached to multiple security policies
468484
statement error 1114
469485
alter table employees modify column name set masking policy mask_int_policy using (name, department);
470486

@@ -810,6 +826,9 @@ SELECT array_transform(arr, x -> x + sensitive) FROM t_test;
810826
statement ok
811827
CREATE MASKING POLICY mask_conditional AS (val INT, threshold INT) RETURNS INT -> CASE WHEN val > threshold THEN val ELSE -999 END;
812828

829+
statement ok
830+
ALTER TABLE t_test MODIFY COLUMN sensitive UNSET MASKING POLICY
831+
813832
statement ok
814833
ALTER TABLE t_test MODIFY COLUMN sensitive SET MASKING POLICY mask_conditional USING (sensitive, public);
815834

0 commit comments

Comments
ย (0)