From b943c29b3543006a55cc92d7b4c0438276512457 Mon Sep 17 00:00:00 2001 From: TCeason Date: Fri, 14 Nov 2025 11:36:09 +0800 Subject: [PATCH 1/3] feat(query):masking policy support rbac --- Cargo.lock | 1 + src/meta/api/src/data_mask_api.rs | 11 + src/meta/api/src/data_mask_api_impl.rs | 34 +++- src/meta/api/src/lib.rs | 2 +- src/meta/api/src/row_access_policy_api.rs | 6 + .../api/src/row_access_policy_api_impl.rs | 31 +++ .../data_mask/data_mask_id_to_name_ident.rs | 58 ++++++ src/meta/app/src/data_mask/mod.rs | 4 + .../app/src/principal/ownership_object.rs | 18 +- .../tenant_ownership_object_ident.rs | 17 ++ src/meta/app/src/principal/user_grant.rs | 7 + src/meta/app/src/principal/user_privilege.rs | 32 ++- src/meta/app/src/row_access_policy/mod.rs | 4 + .../row_access_policy_id_to_name_ident.rs | 58 ++++++ .../src/ownership_from_to_protobuf_impl.rs | 10 + .../src/user_from_to_protobuf_impl.rs | 10 + src/meta/proto-conv/src/util.rs | 1 + src/meta/proto-conv/tests/it/main.rs | 1 + .../it/v159_grant_object_masking_policy.rs | 82 ++++++++ src/meta/protos/proto/ownership.proto | 5 + src/meta/protos/proto/user.proto | 5 + src/query/ast/src/ast/statements/principal.rs | 9 +- src/query/ast/src/ast/statements/user.rs | 25 ++- src/query/ast/src/parser/statement.rs | 47 ++++- src/query/ast/src/parser/token.rs | 2 + src/query/ast/tests/it/parser.rs | 6 + .../ast/tests/it/testdata/stmt-error.txt | 14 +- src/query/ast/tests/it/testdata/stmt.txt | 134 +++++++++++- .../ee/src/data_mask/data_mask_handler.rs | 21 +- .../data_mask/src/data_mask_handler.rs | 8 +- src/query/management/src/role/role_mgr.rs | 1 + .../interpreters/access/privilege_access.rs | 190 +++++++++++++++++- .../service/src/interpreters/common/grant.rs | 13 ++ .../interpreter_data_mask_create.rs | 40 +++- .../interpreter_data_mask_drop.rs | 17 +- .../interpreter_privilege_grant.rs | 5 +- .../show_grants/show_grants_table.rs | 81 +++++++- src/query/sql/Cargo.toml | 1 + .../sql/src/planner/binder/ddl/account.rs | 31 +++ src/query/users/src/user_mgr.rs | 3 +- src/query/users/src/visibility_checker.rs | 34 +++- .../10_0001_masking_policy_rbac.result | 43 ++++ .../10_rbac/10_0001_masking_policy_rbac.sh | 65 ++++++ 43 files changed, 1109 insertions(+), 78 deletions(-) create mode 100644 src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs create mode 100644 src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs create mode 100644 src/meta/proto-conv/tests/it/v159_grant_object_masking_policy.rs create mode 100644 tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.result create mode 100755 tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.sh diff --git a/Cargo.lock b/Cargo.lock index de46032f7b3f3..f65e81b755618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4184,6 +4184,7 @@ dependencies = [ "databend-common-functions", "databend-common-license", "databend-common-management", + "databend-common-meta-api", "databend-common-meta-app", "databend-common-meta-types", "databend-common-metrics", diff --git a/src/meta/api/src/data_mask_api.rs b/src/meta/api/src/data_mask_api.rs index 69d5fd5c451e3..cf2d3d7113e14 100644 --- a/src/meta/api/src/data_mask_api.rs +++ b/src/meta/api/src/data_mask_api.rs @@ -48,6 +48,17 @@ pub trait DatamaskApi: Send + Sync { name_ident: &DataMaskNameIdent, ) -> Result>, MetaError>; + async fn get_data_mask_id( + &self, + name_ident: &DataMaskNameIdent, + ) -> Result>, MetaError>; + + async fn get_data_mask_name_by_id( + &self, + tenant: &Tenant, + policy_id: u64, + ) -> Result, MetaError>; + async fn get_data_mask_by_id( &self, tenant: &Tenant, diff --git a/src/meta/api/src/data_mask_api_impl.rs b/src/meta/api/src/data_mask_api_impl.rs index ff33b0985d887..63ac3cdad0da3 100644 --- a/src/meta/api/src/data_mask_api_impl.rs +++ b/src/meta/api/src/data_mask_api_impl.rs @@ -17,7 +17,9 @@ use databend_common_meta_app::data_mask::CreateDatamaskReply; use databend_common_meta_app::data_mask::CreateDatamaskReq; use databend_common_meta_app::data_mask::DataMaskId; use databend_common_meta_app::data_mask::DataMaskIdIdent; +use databend_common_meta_app::data_mask::DataMaskIdToNameIdent; use databend_common_meta_app::data_mask::DataMaskNameIdent; +use databend_common_meta_app::data_mask::DataMaskNameIdentRaw; use databend_common_meta_app::data_mask::DatamaskMeta; use databend_common_meta_app::data_mask::MaskPolicyIdTableId; use databend_common_meta_app::data_mask::MaskPolicyTableIdIdent; @@ -46,6 +48,7 @@ use crate::txn_condition_util::txn_cond_eq_keys_with_prefix; use crate::txn_condition_util::txn_cond_eq_seq; use crate::txn_core_util::send_txn; use crate::txn_core_util::txn_delete_exact; +use crate::txn_op_builder_util::txn_op_del; use crate::txn_op_builder_util::txn_op_put_pb; /// DatamaskApi is implemented upon kvapi::KVApi. @@ -77,6 +80,8 @@ impl> DatamaskApi for KV { let id = DataMaskId::new(masking_policy_id); let id_ident = DataMaskIdIdent::new_generic(name_ident.tenant(), id); + let id_to_name_ident = DataMaskIdToNameIdent::new_generic(name_ident.tenant(), id); + let name_raw = DataMaskNameIdentRaw::from(name_ident.clone()); let id_list_key = MaskPolicyTableIdListIdent::new_from(name_ident.clone()); debug!( @@ -91,9 +96,11 @@ impl> DatamaskApi for KV { txn.condition.push(txn_cond_eq_seq(name_ident, 0)); txn.condition .push(txn_cond_eq_seq(&row_access_name_ident, 0)); + txn.if_then.extend(vec![ - txn_op_put_pb(name_ident, &id, None)?, // name -> db_id + txn_op_put_pb(name_ident, &id, None)?, // name -> masking_policy_id txn_op_put_pb(&id_ident, &meta, None)?, // id -> meta + txn_op_put_pb(&id_to_name_ident, &name_raw, None)?, // id -> name // TODO: Tentative retention for compatibility MaskPolicyTableIdListIdent related logic. It can be directly deleted later txn_op_put_pb(&id_list_key, &id_list, None)?, // data mask name -> id_list ]); @@ -159,7 +166,9 @@ impl> DatamaskApi for KV { } // No references - drop the policy - let id_ident = seq_id.data.into_t_ident(tenant); + let id_ident = seq_id.data.into_t_ident(tenant.clone()); + let id_to_name_ident = + DataMaskIdToNameIdent::new_generic(tenant, DataMaskId::new(policy_id)); let mut txn = TxnRequest::default(); // Ensure no new references were created @@ -168,6 +177,7 @@ impl> DatamaskApi for KV { txn_delete_exact(&mut txn, name_ident, seq_id.seq); txn_delete_exact(&mut txn, &id_ident, seq_meta.seq); + txn.if_then.push(txn_op_del(&id_to_name_ident)); // TODO: Tentative retention for compatibility. Can be deleted later. clear_table_column_mask_policy(self, name_ident, &mut txn).await?; @@ -191,6 +201,26 @@ impl> DatamaskApi for KV { Ok(res.map(|(_, seq_meta)| seq_meta)) } + async fn get_data_mask_id( + &self, + name_ident: &DataMaskNameIdent, + ) -> Result>, MetaError> { + self.get_pb(name_ident).await + } + + async fn get_data_mask_name_by_id( + &self, + tenant: &Tenant, + policy_id: u64, + ) -> Result, MetaError> { + let ident = DataMaskIdToNameIdent::new_generic(tenant.clone(), DataMaskId::new(policy_id)); + let seq_meta = self.get_pb(&ident).await?; + + debug!(ident :% =(&ident); "get_data_mask_name_by_id"); + + Ok(seq_meta.map(|s| s.data.data_mask_name().to_string())) + } + async fn get_data_mask_by_id( &self, tenant: &Tenant, diff --git a/src/meta/api/src/lib.rs b/src/meta/api/src/lib.rs index 79c5c6844647a..8fc901d875c0d 100644 --- a/src/meta/api/src/lib.rs +++ b/src/meta/api/src/lib.rs @@ -19,7 +19,7 @@ extern crate databend_common_meta_types; pub mod catalog_api; -mod data_mask_api; +pub mod data_mask_api; mod data_mask_api_impl; pub mod data_retention_util; mod database_api; diff --git a/src/meta/api/src/row_access_policy_api.rs b/src/meta/api/src/row_access_policy_api.rs index 25526d67056ac..d534f53510b86 100644 --- a/src/meta/api/src/row_access_policy_api.rs +++ b/src/meta/api/src/row_access_policy_api.rs @@ -51,6 +51,12 @@ pub trait RowAccessPolicyApi: Send + Sync { name_ident: &RowAccessPolicyNameIdent, ) -> Result, SeqV)>, MetaError>; + async fn get_row_access_policy_name_by_id( + &self, + tenant: &Tenant, + policy_id: u64, + ) -> Result, MetaError>; + async fn get_row_access_policy_by_id( &self, tenant: &Tenant, diff --git a/src/meta/api/src/row_access_policy_api_impl.rs b/src/meta/api/src/row_access_policy_api_impl.rs index 87bffe04435d6..3453f63134930 100644 --- a/src/meta/api/src/row_access_policy_api_impl.rs +++ b/src/meta/api/src/row_access_policy_api_impl.rs @@ -20,8 +20,10 @@ use databend_common_meta_app::row_access_policy::CreateRowAccessPolicyReply; use databend_common_meta_app::row_access_policy::CreateRowAccessPolicyReq; use databend_common_meta_app::row_access_policy::RowAccessPolicyId; use databend_common_meta_app::row_access_policy::RowAccessPolicyIdIdent; +use databend_common_meta_app::row_access_policy::RowAccessPolicyIdToNameIdent; use databend_common_meta_app::row_access_policy::RowAccessPolicyMeta; use databend_common_meta_app::row_access_policy::RowAccessPolicyNameIdent; +use databend_common_meta_app::row_access_policy::RowAccessPolicyNameIdentRaw; use databend_common_meta_app::row_access_policy::RowAccessPolicyTableIdIdent; use databend_common_meta_app::tenant::Tenant; use databend_common_meta_app::tenant_key::errors::ExistError; @@ -44,6 +46,7 @@ use crate::txn_condition_util::txn_cond_eq_keys_with_prefix; use crate::txn_condition_util::txn_cond_eq_seq; use crate::txn_core_util::send_txn; use crate::txn_core_util::txn_delete_exact; +use crate::txn_op_builder_util::txn_op_del; use crate::txn_op_builder_util::txn_op_put_pb; /// RowAccessPolicyApi is implemented upon kvapi::KVApi. @@ -73,8 +76,12 @@ impl> RowAccessPolicyApi for KV { // Create row policy by inserting these record: // name -> id // id -> policy + // id -> name let id_ident = RowAccessPolicyIdIdent::new_generic(name_ident.tenant(), policy_id); + let id_to_name_ident = + RowAccessPolicyIdToNameIdent::new_generic(name_ident.tenant(), policy_id); + let name_raw = RowAccessPolicyNameIdentRaw::from(name_ident.clone()); debug!( id :? =(&id_ident), @@ -89,6 +96,7 @@ impl> RowAccessPolicyApi for KV { txn.if_then.extend(vec![ txn_op_put_pb(name_ident, &policy_id, None)?, // name -> policy_id txn_op_put_pb(&id_ident, &meta, None)?, // id -> meta + txn_op_put_pb(&id_to_name_ident, &name_raw, None)?, // id -> name ]); } @@ -151,6 +159,10 @@ impl> RowAccessPolicyApi for KV { // No references - drop the policy let id_ident = seq_id.data.into_t_ident(tenant); + let id_to_name_ident = RowAccessPolicyIdToNameIdent::new_generic( + tenant.clone(), + RowAccessPolicyId::new(policy_id), + ); let mut txn = TxnRequest::default(); // Ensure no new references were created @@ -159,6 +171,7 @@ impl> RowAccessPolicyApi for KV { txn_delete_exact(&mut txn, name_ident, seq_id.seq); txn_delete_exact(&mut txn, &id_ident, seq_meta.seq); + txn.if_then.push(txn_op_del(&id_to_name_ident)); let (succ, _responses) = send_txn(self, txn).await?; if succ { @@ -179,6 +192,24 @@ impl> RowAccessPolicyApi for KV { Ok(res) } + async fn get_row_access_policy_name_by_id( + &self, + tenant: &Tenant, + policy_id: u64, + ) -> Result, MetaError> { + let ident = RowAccessPolicyIdToNameIdent::new_generic( + tenant.clone(), + RowAccessPolicyId::new(policy_id), + ); + let seq_meta = self.get_pb(&ident).await?; + + debug!(ident :% =(&ident); "get_row_access_policy_name_by_id"); + + let name = seq_meta.map(|s| s.data.row_access_name().to_string()); + + Ok(name) + } + async fn get_row_access_policy_by_id( &self, tenant: &Tenant, diff --git a/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs b/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs new file mode 100644 index 0000000000000..ee792c7420e9e --- /dev/null +++ b/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs @@ -0,0 +1,58 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed 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. + +use crate::data_mask::DataMaskId; +use crate::tenant_key::ident::TIdent; +use crate::tenant_key::raw::TIdentRaw; + +pub type DataMaskIdToNameIdent = TIdent; +pub type DataMaskIdToNameIdentRaw = TIdentRaw; + +pub use kvapi_impl::Resource; + +impl DataMaskIdToNameIdent { + pub fn data_mask_id(&self) -> DataMaskId { + *self.name() + } +} + +impl DataMaskIdToNameIdentRaw { + pub fn data_mask_id(&self) -> DataMaskId { + *self.name() + } +} + +mod kvapi_impl { + + use databend_common_meta_kvapi::kvapi; + + use crate::data_mask::data_mask_id_to_name_ident::DataMaskIdToNameIdent; + use crate::data_mask::data_mask_name_ident::DataMaskNameIdentRaw; + use crate::tenant_key::resource::TenantResource; + + pub struct Resource; + impl TenantResource for Resource { + const PREFIX: &'static str = "__fd_datamask_id_to_name"; + const TYPE: &'static str = "DataMaskIdToNameIdent"; + const HAS_TENANT: bool = false; + type ValueType = DataMaskNameIdentRaw; + } + + impl kvapi::Value for DataMaskNameIdentRaw { + type KeyType = DataMaskIdToNameIdent; + fn dependency_keys(&self, _key: &Self::KeyType) -> impl IntoIterator { + [] + } + } +} diff --git a/src/meta/app/src/data_mask/mod.rs b/src/meta/app/src/data_mask/mod.rs index 671ce268750d3..f3561ec1d81e7 100644 --- a/src/meta/app/src/data_mask/mod.rs +++ b/src/meta/app/src/data_mask/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod data_mask_id_ident; +pub mod data_mask_id_to_name_ident; pub mod data_mask_name_ident; pub mod mask_policy_policy_table_id_ident; pub mod mask_policy_table_id_list_ident; @@ -23,7 +24,10 @@ use chrono::DateTime; use chrono::Utc; pub use data_mask_id_ident::DataMaskId; pub use data_mask_id_ident::DataMaskIdIdent; +pub use data_mask_id_to_name_ident::DataMaskIdToNameIdent; +pub use data_mask_id_to_name_ident::DataMaskIdToNameIdentRaw; pub use data_mask_name_ident::DataMaskNameIdent; +pub use data_mask_name_ident::DataMaskNameIdentRaw; pub use mask_policy_policy_table_id_ident::MaskPolicyIdTableId; pub use mask_policy_policy_table_id_ident::MaskPolicyTableIdIdent; pub use mask_policy_table_id_list_ident::MaskPolicyTableIdListIdent; diff --git a/src/meta/app/src/principal/ownership_object.rs b/src/meta/app/src/principal/ownership_object.rs index 4c2019f5c2875..94442ab92b406 100644 --- a/src/meta/app/src/principal/ownership_object.rs +++ b/src/meta/app/src/principal/ownership_object.rs @@ -30,6 +30,8 @@ use databend_common_meta_kvapi::kvapi::KeyCodec; /// - `table-by-catalog-id//` /// - `stage-by-name/` /// - `udf-by-name/` +/// - `procedure-by-id/` +/// - `masking-policy-by-id/` #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq, Hash)] pub enum OwnershipObject { /// used on the fuse databases @@ -68,6 +70,10 @@ pub enum OwnershipObject { Procedure { procedure_id: u64, }, + + MaskingPolicy { + policy_id: u64, + }, } impl OwnershipObject { @@ -100,6 +106,9 @@ impl fmt::Display for OwnershipObject { OwnershipObject::Connection { name } => write!(f, "CONNECTION {name}"), OwnershipObject::Sequence { name } => write!(f, "SEQUENCE {name}"), OwnershipObject::Procedure { procedure_id } => write!(f, "PROCEDURE {procedure_id}"), + OwnershipObject::MaskingPolicy { policy_id } => { + write!(f, "MASKING POLICY {policy_id}") + } } } } @@ -145,6 +154,9 @@ impl KeyCodec for OwnershipObject { OwnershipObject::Procedure { procedure_id } => { b.push_raw("procedure-by-id").push_u64(*procedure_id) } + OwnershipObject::MaskingPolicy { policy_id } => { + b.push_raw("masking-policy-by-id").push_u64(*policy_id) + } } } @@ -207,9 +219,13 @@ impl KeyCodec for OwnershipObject { let procedure_id = p.next_u64()?; Ok(OwnershipObject::Procedure { procedure_id }) } + "masking-policy-by-id" => { + let policy_id = p.next_u64()?; + Ok(OwnershipObject::MaskingPolicy { policy_id }) + } _ => Err(kvapi::KeyError::InvalidSegment { i: p.index(), - expect: "database-by-id|database-by-catalog-id|table-by-id|table-by-catalog-id|stage-by-name|udf-by-name|warehouse-by-id|connection-by-name" + expect: "database-by-id|database-by-catalog-id|table-by-id|table-by-catalog-id|stage-by-name|udf-by-name|warehouse-by-id|connection-by-name|masking-policy-by-id" .to_string(), got: q.to_string(), }), diff --git a/src/meta/app/src/principal/tenant_ownership_object_ident.rs b/src/meta/app/src/principal/tenant_ownership_object_ident.rs index edfa9b565dac7..c085514351d23 100644 --- a/src/meta/app/src/principal/tenant_ownership_object_ident.rs +++ b/src/meta/app/src/principal/tenant_ownership_object_ident.rs @@ -271,6 +271,23 @@ mod tests { let parsed = TenantOwnershipObjectIdent::from_str_key(&key).unwrap(); assert_eq!(role_grantee, parsed); } + + // masking policy + { + let role_grantee = TenantOwnershipObjectIdent::new_unchecked( + Tenant::new_literal("tenant_mask"), + OwnershipObject::MaskingPolicy { policy_id: 99 }, + ); + + let key = role_grantee.to_string_key(); + assert_eq!( + "__fd_object_owners/tenant_mask/masking-policy-by-id/99", + key + ); + + let parsed = TenantOwnershipObjectIdent::from_str_key(&key).unwrap(); + assert_eq!(role_grantee, parsed); + } } #[test] diff --git a/src/meta/app/src/principal/user_grant.rs b/src/meta/app/src/principal/user_grant.rs index 8bbe73768e6ac..1f1829891b289 100644 --- a/src/meta/app/src/principal/user_grant.rs +++ b/src/meta/app/src/principal/user_grant.rs @@ -63,6 +63,7 @@ pub enum GrantObject { Connection(String), Sequence(String), Procedure(u64), + MaskingPolicy(u64), } impl GrantObject { @@ -99,6 +100,7 @@ impl GrantObject { (GrantObject::Connection(c), GrantObject::Connection(rc)) => c == rc, (GrantObject::Sequence(s), GrantObject::Sequence(rs)) => s == rs, (GrantObject::Procedure(p), GrantObject::Procedure(rp)) => p == rp, + (GrantObject::MaskingPolicy(lp), GrantObject::MaskingPolicy(rp)) => lp == rp, _ => false, } } @@ -131,6 +133,9 @@ impl GrantObject { GrantObject::Procedure(_) => { UserPrivilegeSet::available_privileges_on_procedure(available_ownership) } + GrantObject::MaskingPolicy(_) => { + UserPrivilegeSet::available_privileges_on_masking_policy(available_ownership) + } } } @@ -142,6 +147,7 @@ impl GrantObject { | GrantObject::Warehouse(_) | GrantObject::Sequence(_) | GrantObject::Procedure(_) + | GrantObject::MaskingPolicy(_) | GrantObject::Connection(_) => None, GrantObject::Database(cat, _) | GrantObject::DatabaseById(cat, _) => Some(cat.clone()), GrantObject::Table(cat, _, _) | GrantObject::TableById(cat, _, _) => Some(cat.clone()), @@ -167,6 +173,7 @@ impl fmt::Display for GrantObject { GrantObject::Connection(c) => write!(f, "CONNECTION {c}"), GrantObject::Sequence(s) => write!(f, "SEQUENCE {s}"), GrantObject::Procedure(p) => write!(f, "PROCEDURE {p}"), + GrantObject::MaskingPolicy(policy_id) => write!(f, "MASKING POLICY {policy_id}"), } } } diff --git a/src/meta/app/src/principal/user_privilege.rs b/src/meta/app/src/principal/user_privilege.rs index 30065dabe63d7..37960e7a12c50 100644 --- a/src/meta/app/src/principal/user_privilege.rs +++ b/src/meta/app/src/principal/user_privilege.rs @@ -66,8 +66,6 @@ pub enum UserPrivilegeType { DropRole = 1 << 14, // Privilege to Drop user. DropUser = 1 << 15, - // Privilege to Create/Drop DataMask. - CreateDataMask = 1 << 16, // Privilege to Own a databend object such as database/table. Ownership = 1 << 17, // Privilege to Read stage @@ -90,8 +88,13 @@ pub enum UserPrivilegeType { CreateProcedure = 1 << 26, // Privilege to Access Procedure AccessProcedure = 1 << 27, + // Privilege to Apply Masking Policy. + ApplyMaskingPolicy = 1 << 28, + // Privilege to Create Masking Policy. + CreateMaskingPolicy = 1 << 29, // Discard Privilege Type Set = 1 << 4, + CreateDataMask = 1 << 16, } const ALL_PRIVILEGES: BitFlags = make_bitflags!( @@ -112,6 +115,8 @@ const ALL_PRIVILEGES: BitFlags = make_bitflags!( | CreateStage | Set | CreateDataMask + | CreateMaskingPolicy + | ApplyMaskingPolicy | Ownership | Read | Write @@ -142,6 +147,8 @@ impl Display for UserPrivilegeType { UserPrivilegeType::Grant => "GRANT", UserPrivilegeType::Set => "SET", UserPrivilegeType::CreateDataMask => "CREATE DATAMASK", + UserPrivilegeType::CreateMaskingPolicy => "CREATE MASKING POLICY", + UserPrivilegeType::ApplyMaskingPolicy => "APPLY MASKING POLICY", UserPrivilegeType::Ownership => "OWNERSHIP", UserPrivilegeType::Read => "Read", UserPrivilegeType::Write => "Write", @@ -181,8 +188,11 @@ impl From for UserPrivilegeType { } databend_common_ast::ast::UserPrivilegeType::DropRole => UserPrivilegeType::DropRole, databend_common_ast::ast::UserPrivilegeType::DropUser => UserPrivilegeType::DropUser, - databend_common_ast::ast::UserPrivilegeType::CreateDataMask => { - UserPrivilegeType::CreateDataMask + databend_common_ast::ast::UserPrivilegeType::CreateMaskingPolicy => { + UserPrivilegeType::CreateMaskingPolicy + } + databend_common_ast::ast::UserPrivilegeType::ApplyMaskingPolicy => { + UserPrivilegeType::ApplyMaskingPolicy } databend_common_ast::ast::UserPrivilegeType::Ownership => UserPrivilegeType::Ownership, databend_common_ast::ast::UserPrivilegeType::Read => UserPrivilegeType::Read, @@ -247,14 +257,16 @@ impl UserPrivilegeSet { let wh_privs_without_ownership = Self::available_privileges_on_warehouse(false); let connection_privs_without_ownership = Self::available_privileges_on_connection(false); let seq_privs_without_ownership = Self::available_privileges_on_sequence(false); - let privs = make_bitflags!(UserPrivilegeType::{ Usage | Super | CreateUser | DropUser | CreateRole | DropRole | CreateDatabase | Grant | CreateDataMask | CreateWarehouse | CreateConnection | CreateSequence | CreateProcedure }); + let mask_privs = Self::available_privileges_on_masking_policy(false); + let privs = make_bitflags!(UserPrivilegeType::{ Usage | Super | CreateUser | DropUser | CreateRole | DropRole | CreateDatabase | Grant | CreateDataMask | CreateMaskingPolicy | CreateWarehouse | CreateConnection | CreateSequence | CreateProcedure }); (database_privs.privileges | privs | stage_privs_without_ownership.privileges | wh_privs_without_ownership.privileges | connection_privs_without_ownership.privileges | seq_privs_without_ownership.privileges - | udf_privs_without_ownership.privileges) + | udf_privs_without_ownership.privileges + | mask_privs.privileges) .into() } @@ -325,6 +337,14 @@ impl UserPrivilegeSet { } } + pub fn available_privileges_on_masking_policy(available_ownership: bool) -> Self { + if available_ownership { + make_bitflags!(UserPrivilegeType::{ ApplyMaskingPolicy | Ownership }).into() + } else { + make_bitflags!(UserPrivilegeType::{ ApplyMaskingPolicy }).into() + } + } + // TODO: remove this, as ALL has different meanings on different objects pub fn all_privileges() -> Self { ALL_PRIVILEGES.into() diff --git a/src/meta/app/src/row_access_policy/mod.rs b/src/meta/app/src/row_access_policy/mod.rs index 9cad48b92da9d..9ee06fc7e2f01 100644 --- a/src/meta/app/src/row_access_policy/mod.rs +++ b/src/meta/app/src/row_access_policy/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. mod row_access_policy_id_ident; +pub mod row_access_policy_id_to_name_ident; pub mod row_access_policy_name_ident; pub mod row_access_policy_table_id_ident; @@ -20,7 +21,10 @@ use chrono::DateTime; use chrono::Utc; pub use row_access_policy_id_ident::RowAccessPolicyId; pub use row_access_policy_id_ident::RowAccessPolicyIdIdent; +pub use row_access_policy_id_to_name_ident::RowAccessPolicyIdToNameIdent; +pub use row_access_policy_id_to_name_ident::RowAccessPolicyIdToNameIdentRaw; pub use row_access_policy_name_ident::RowAccessPolicyNameIdent; +pub use row_access_policy_name_ident::RowAccessPolicyNameIdentRaw; pub use row_access_policy_table_id_ident::RowAccessPolicyTableIdIdent; #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs b/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs new file mode 100644 index 0000000000000..56de59c1e3ea6 --- /dev/null +++ b/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs @@ -0,0 +1,58 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed 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. + +use crate::row_access_policy::RowAccessPolicyId; +use crate::tenant_key::ident::TIdent; +use crate::tenant_key::raw::TIdentRaw; + +pub type RowAccessPolicyIdToNameIdent = TIdent; +pub type RowAccessPolicyIdToNameIdentRaw = TIdentRaw; + +pub use kvapi_impl::Resource; + +impl RowAccessPolicyIdToNameIdent { + pub fn row_access_policy_id(&self) -> RowAccessPolicyId { + *self.name() + } +} + +impl RowAccessPolicyIdToNameIdentRaw { + pub fn row_access_policy_id(&self) -> RowAccessPolicyId { + *self.name() + } +} + +mod kvapi_impl { + + use databend_common_meta_kvapi::kvapi; + + use crate::row_access_policy::row_access_policy_id_to_name_ident::RowAccessPolicyIdToNameIdent; + use crate::row_access_policy::RowAccessPolicyNameIdentRaw; + use crate::tenant_key::resource::TenantResource; + + pub struct Resource; + impl TenantResource for Resource { + const PREFIX: &'static str = "__fd_row_access_policy_id_to_name"; + const TYPE: &'static str = "RowAccessPolicyIdToNameIdent"; + const HAS_TENANT: bool = false; + type ValueType = RowAccessPolicyNameIdentRaw; + } + + impl kvapi::Value for RowAccessPolicyNameIdentRaw { + type KeyType = RowAccessPolicyIdToNameIdent; + fn dependency_keys(&self, _key: &Self::KeyType) -> impl IntoIterator { + [] + } + } +} diff --git a/src/meta/proto-conv/src/ownership_from_to_protobuf_impl.rs b/src/meta/proto-conv/src/ownership_from_to_protobuf_impl.rs index fc673c45475b6..e7c81d55d74ad 100644 --- a/src/meta/proto-conv/src/ownership_from_to_protobuf_impl.rs +++ b/src/meta/proto-conv/src/ownership_from_to_protobuf_impl.rs @@ -100,6 +100,9 @@ impl FromToProto for mt::principal::OwnershipObject { pb::ownership_object::Object::Procedure( pb::ownership_object::OwnershipProcedureObject { procedure_id }, ) => Ok(mt::principal::OwnershipObject::Procedure { procedure_id }), + pb::ownership_object::Object::MaskingPolicy( + pb::ownership_object::OwnershipMaskingPolicyObject { policy_id }, + ) => Ok(mt::principal::OwnershipObject::MaskingPolicy { policy_id }), } } @@ -161,6 +164,13 @@ impl FromToProto for mt::principal::OwnershipObject { }, )) } + mt::principal::OwnershipObject::MaskingPolicy { policy_id } => { + Some(pb::ownership_object::Object::MaskingPolicy( + pb::ownership_object::OwnershipMaskingPolicyObject { + policy_id: *policy_id, + }, + )) + } }; Ok(pb::OwnershipObject { ver: VER, diff --git a/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs b/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs index de848c11245c9..cd8794051c93d 100644 --- a/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs +++ b/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs @@ -203,6 +203,9 @@ impl FromToProto for mt::principal::GrantObject { pb::grant_object::Object::Procedure(pb::grant_object::GrantProcedureObject { procedure_id, }) => Ok(mt::principal::GrantObject::Procedure(procedure_id)), + pb::grant_object::Object::Maskingpolicy( + pb::grant_object::GrantMaskingPolicyObject { policy_id }, + ) => Ok(mt::principal::GrantObject::MaskingPolicy(policy_id)), } } @@ -263,6 +266,13 @@ impl FromToProto for mt::principal::GrantObject { mt::principal::GrantObject::Procedure(p) => Some(pb::grant_object::Object::Procedure( pb::grant_object::GrantProcedureObject { procedure_id: *p }, )), + mt::principal::GrantObject::MaskingPolicy(policy_id) => { + Some(pb::grant_object::Object::Maskingpolicy( + pb::grant_object::GrantMaskingPolicyObject { + policy_id: *policy_id, + }, + )) + } }; Ok(pb::GrantObject { ver: VER, diff --git a/src/meta/proto-conv/src/util.rs b/src/meta/proto-conv/src/util.rs index f43acf20a64cf..87071c3c44e02 100644 --- a/src/meta/proto-conv/src/util.rs +++ b/src/meta/proto-conv/src/util.rs @@ -188,6 +188,7 @@ const META_CHANGE_LOG: &[(u64, &str)] = &[ (156, "2025-10-22: Add: DataMaskMeta add DataMaskArg"), (157, "2025-10-22: Add: TableDataType TimestampTz"), (158, "2025-10-22: Add: Server UDTF"), + (159, "2025-11-18: Add: Grant/OwnershipMaskingPolicyObject and masking policy privileges"), // Dear developer: // If you're gonna add a new metadata version, you'll have to add a test for it. // You could just copy an existing test file(e.g., `../tests/it/v024_table_meta.rs`) diff --git a/src/meta/proto-conv/tests/it/main.rs b/src/meta/proto-conv/tests/it/main.rs index dbd4618aa3ea4..9c97256f2f524 100644 --- a/src/meta/proto-conv/tests/it/main.rs +++ b/src/meta/proto-conv/tests/it/main.rs @@ -150,3 +150,4 @@ mod v155_row_access_policy_args; mod v156_data_mask_args; mod v157_type_timestamp_tz; mod v158_udtf_server; +mod v159_grant_object_masking_policy; diff --git a/src/meta/proto-conv/tests/it/v159_grant_object_masking_policy.rs b/src/meta/proto-conv/tests/it/v159_grant_object_masking_policy.rs new file mode 100644 index 0000000000000..d139d0aaea17b --- /dev/null +++ b/src/meta/proto-conv/tests/it/v159_grant_object_masking_policy.rs @@ -0,0 +1,82 @@ +// Copyright 2023 Datafuse Labs. +// +// Licensed 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. + +use std::collections::HashSet; + +use chrono::DateTime; +use chrono::Utc; +use databend_common_meta_app as mt; +use databend_common_meta_app::principal::OwnershipInfo; +use databend_common_meta_app::principal::OwnershipObject; +use databend_common_meta_app::principal::UserGrantSet; +use databend_common_meta_app::principal::UserPrivilegeType; +use enumflags2::make_bitflags; +use fastrace::func_name; + +use crate::common; + +#[test] +fn test_decode_v159_grant_object() -> anyhow::Result<()> { + let role_info_v159 = vec![ + 10, 9, 109, 97, 115, 107, 95, 114, 111, 108, 101, 18, 59, 10, 22, 10, 9, 10, 0, 160, 6, + 159, 1, 168, 6, 24, 16, 128, 128, 4, 160, 6, 159, 1, 168, 6, 24, 10, 26, 10, 11, 98, 2, 8, + 7, 160, 6, 159, 1, 168, 6, 24, 16, 128, 128, 128, 128, 1, 160, 6, 159, 1, 168, 6, 24, 160, + 6, 159, 1, 168, 6, 24, 26, 23, 49, 57, 55, 48, 45, 48, 49, 45, 48, 49, 32, 48, 48, 58, 48, + 48, 58, 48, 48, 32, 85, 84, 67, 34, 23, 49, 57, 55, 48, 45, 48, 49, 45, 48, 49, 32, 48, 48, + 58, 48, 48, 58, 48, 48, 32, 85, 84, 67, 160, 6, 159, 1, 168, 6, 24, + ]; + + let want = || mt::principal::RoleInfo { + name: "mask_role".to_string(), + comment: None, + grants: UserGrantSet::new( + vec![ + mt::principal::GrantEntry::new( + mt::principal::GrantObject::Global, + make_bitflags!(UserPrivilegeType::{CreateDataMask}), + ), + mt::principal::GrantEntry::new( + mt::principal::GrantObject::MaskingPolicy(7), + make_bitflags!(UserPrivilegeType::{ApplyMaskingPolicy}), + ), + ], + HashSet::new(), + ), + created_on: DateTime::::default(), + update_on: DateTime::::default(), + }; + + common::test_pb_from_to(func_name!(), want())?; + common::test_load_old(func_name!(), role_info_v159.as_slice(), 159, want())?; + + Ok(()) +} + +#[test] +fn test_decode_v159_masking_policy_ownership() -> anyhow::Result<()> { + let ownership_info_v159 = vec![ + 10, 10, 111, 119, 110, 101, 114, 95, 114, 111, 108, 101, 18, 11, 74, 2, 8, 42, 160, 6, 159, + 1, 168, 6, 24, 160, 6, 159, 1, 168, 6, 24, + ]; + + let want = || OwnershipInfo { + role: "owner_role".to_string(), + object: OwnershipObject::MaskingPolicy { policy_id: 42 }, + }; + + common::test_pb_from_to(func_name!(), want())?; + common::test_load_old(func_name!(), ownership_info_v159.as_slice(), 159, want())?; + + Ok(()) +} diff --git a/src/meta/protos/proto/ownership.proto b/src/meta/protos/proto/ownership.proto index 10be5ece958d0..67022eb19a53e 100644 --- a/src/meta/protos/proto/ownership.proto +++ b/src/meta/protos/proto/ownership.proto @@ -62,6 +62,10 @@ message OwnershipObject { uint64 procedure_id = 1; } + message OwnershipMaskingPolicyObject { + uint64 policy_id = 1; + } + oneof object { OwnershipDatabaseObject database = 1; OwnershipTableObject table = 2; @@ -71,5 +75,6 @@ message OwnershipObject { OwnershipConnectionObject connection = 6; OwnershipSequenceObject sequence = 7; OwnershipProcedureObject procedure = 8; + OwnershipMaskingPolicyObject masking_policy = 9; } } diff --git a/src/meta/protos/proto/user.proto b/src/meta/protos/proto/user.proto index 3c9d25d5c7276..ecaaf8d311e36 100644 --- a/src/meta/protos/proto/user.proto +++ b/src/meta/protos/proto/user.proto @@ -93,6 +93,10 @@ message GrantObject { uint64 procedure_id = 1; } + message GrantMaskingPolicyObject { + uint64 policy_id = 1; + } + oneof object { GrantGlobalObject global = 1; GrantDatabaseObject database = 2; @@ -105,6 +109,7 @@ message GrantObject { GrantConnectionObject connection = 9; GrantSequenceObject sequence = 10; GrantProcedureObject procedure = 11; + GrantMaskingPolicyObject maskingpolicy = 12; } } diff --git a/src/query/ast/src/ast/statements/principal.rs b/src/query/ast/src/ast/statements/principal.rs index 6a37f9a6548c8..1640f159c2d13 100644 --- a/src/query/ast/src/ast/statements/principal.rs +++ b/src/query/ast/src/ast/statements/principal.rs @@ -140,8 +140,10 @@ pub enum UserPrivilegeType { DropRole, // Privilege to Drop user. DropUser, - // Privilege to Create/Drop DataMask. - CreateDataMask, + // Privilege to Create/Drop Masking Policy. + CreateMaskingPolicy, + // Privilege to Apply Masking Policy. + ApplyMaskingPolicy, // Privilege to Own a databend object such as database/table. Ownership, // Privilege to Read stage @@ -187,7 +189,8 @@ impl Display for UserPrivilegeType { UserPrivilegeType::CreateStage => "CREATE STAGE", UserPrivilegeType::Grant => "GRANT", UserPrivilegeType::Set => "SET", - UserPrivilegeType::CreateDataMask => "CREATE DATAMASK", + UserPrivilegeType::CreateMaskingPolicy => "CREATE MASKING POLICY", + UserPrivilegeType::ApplyMaskingPolicy => "APPLY MASKING POLICY", UserPrivilegeType::Ownership => "OWNERSHIP", UserPrivilegeType::Read => "Read", UserPrivilegeType::Write => "Write", diff --git a/src/query/ast/src/ast/statements/user.rs b/src/query/ast/src/ast/statements/user.rs index eb7467c0982f5..83ae6bd09d710 100644 --- a/src/query/ast/src/ast/statements/user.rs +++ b/src/query/ast/src/ast/statements/user.rs @@ -169,6 +169,7 @@ pub enum GrantObjectName { Connection(String), Sequence(String), Procedure(ProcedureIdentity), + MaskingPolicy(String), } impl Display for GrantObjectName { @@ -184,12 +185,13 @@ impl Display for GrantObjectName { write!(f, "TABLE {table_name}") } } - GrantObjectName::UDF(udf) => write!(f, " UDF {udf}"), - GrantObjectName::Stage(stage) => write!(f, " STAGE {stage}"), - GrantObjectName::Warehouse(w) => write!(f, " WAREHOUSE {w}"), - GrantObjectName::Connection(c) => write!(f, " CONNECTION {c}"), - GrantObjectName::Sequence(s) => write!(f, " SEQUENCE {s}"), - GrantObjectName::Procedure(p) => write!(f, " PROCEDURE {p}"), + GrantObjectName::UDF(udf) => write!(f, "UDF {udf}"), + GrantObjectName::Stage(stage) => write!(f, "STAGE {stage}"), + GrantObjectName::Warehouse(w) => write!(f, "WAREHOUSE {w}"), + GrantObjectName::Connection(c) => write!(f, "CONNECTION {c}"), + GrantObjectName::Sequence(s) => write!(f, "SEQUENCE {s}"), + GrantObjectName::Procedure(p) => write!(f, "PROCEDURE {p}"), + GrantObjectName::MaskingPolicy(policy) => write!(f, "MASKING POLICY {policy}"), } } } @@ -224,6 +226,15 @@ impl Display for AccountMgrSource { match self { AccountMgrSource::Role { role } => write!(f, " ROLE '{role}'")?, AccountMgrSource::Privs { privileges, level } => { + if privileges.len() == 1 + && privileges[0] == UserPrivilegeType::ApplyMaskingPolicy + && matches!(level, AccountMgrLevel::MaskingPolicy(_)) + { + if let AccountMgrLevel::MaskingPolicy(policy) = level { + write!(f, " APPLY ON MASKING POLICY {policy}")?; + return Ok(()); + } + } write!(f, " ")?; write_comma_separated_list(f, privileges.iter().map(|p| p.to_string()))?; write!(f, " ON")?; @@ -250,6 +261,7 @@ pub enum AccountMgrLevel { Connection(String), Sequence(String), Procedure(ProcedureIdentity), + MaskingPolicy(String), } impl Display for AccountMgrLevel { @@ -276,6 +288,7 @@ impl Display for AccountMgrLevel { AccountMgrLevel::Connection(c) => write!(f, " CONNECTION {c}"), AccountMgrLevel::Sequence(s) => write!(f, " SEQUENCE {s}"), AccountMgrLevel::Procedure(p) => write!(f, " PROCEDURE {p}"), + AccountMgrLevel::MaskingPolicy(policy) => write!(f, " MASKING POLICY {policy}"), } } } diff --git a/src/query/ast/src/parser/statement.rs b/src/query/ast/src/parser/statement.rs index abb0ed475c9d7..85922cf505e22 100644 --- a/src/query/ast/src/parser/statement.rs +++ b/src/query/ast/src/parser/statement.rs @@ -3697,6 +3697,26 @@ pub fn grant_source(i: Input) -> IResult { }, ); + let masking_policy_privs = map( + rule! { + APPLY ~ ON ~ MASKING ~ POLICY ~ #ident + }, + |(_, _, _, _, name)| AccountMgrSource::Privs { + privileges: vec![UserPrivilegeType::ApplyMaskingPolicy], + level: AccountMgrLevel::MaskingPolicy(name.to_string()), + }, + ); + + let masking_policy_all_privs = map( + rule! { + ALL ~ PRIVILEGES? ~ ON ~ MASKING ~ POLICY ~ #ident + }, + |(_, _, _, _, _, name)| AccountMgrSource::Privs { + privileges: vec![UserPrivilegeType::ApplyMaskingPolicy], + level: AccountMgrLevel::MaskingPolicy(name.to_string()), + }, + ); + rule!( #role : "ROLE " | #warehouse_all_privs: "ALL [ PRIVILEGES ] ON WAREHOUSE " @@ -3706,6 +3726,8 @@ pub fn grant_source(i: Input) -> IResult { | #warehouse_privs: "USAGE ON WAREHOUSE " | #connection_privs: "ACCESS CONNECTION ON CONNECTION " | #seq_privs: "ACCESS SEQUENCE ON CONNECTION " + | #masking_policy_privs: "APPLY ON MASKING POLICY " + | #masking_policy_all_privs: "ALL [ PRIVILEGES ] ON MASKING POLICY " | #privs : " ON " | #stage_privs : " ON STAGE " | #udf_all_privs: "ALL [ PRIVILEGES ] ON UDF " @@ -3765,6 +3787,14 @@ pub fn priv_type(i: Input) -> IResult { let set = value(UserPrivilegeType::Set, rule! { SET }); let drop = value(UserPrivilegeType::Drop, rule! { DROP }); let create = value(UserPrivilegeType::Create, rule! { CREATE }); + let create_masking_policy = value( + UserPrivilegeType::CreateMaskingPolicy, + rule! { CREATE ~ MASKING ~ POLICY }, + ); + let apply_masking_policy = value( + UserPrivilegeType::ApplyMaskingPolicy, + rule! { APPLY ~ MASKING ~ POLICY }, + ); alt(( rule!( @@ -3793,6 +3823,8 @@ pub fn priv_type(i: Input) -> IResult { | #create_stage | #set | #drop + | #create_masking_policy + | #apply_masking_policy | #create ), )) @@ -3871,6 +3903,10 @@ pub fn on_object_name(i: Input) -> IResult { }, ); + let masking_policy = map(rule! { MASKING ~ POLICY ~ #ident }, |(_, _, name)| { + GrantObjectName::MaskingPolicy(name.to_string()) + }); + rule!( #database : "DATABASE " | #table : "TABLE ." @@ -3880,6 +3916,7 @@ pub fn on_object_name(i: Input) -> IResult { | #connection : "CONNECTION " | #seq : "SEQUENCE " | #procedure : "PROCEDURE " + | #masking_policy : "MASKING POLICY " ) .parse(i) } @@ -3906,10 +3943,15 @@ pub fn grant_level(i: Input) -> IResult { }, ); + let masking_policy = map(rule! { MASKING ~ POLICY ~ #ident }, |(_, _, name)| { + AccountMgrLevel::MaskingPolicy(name.to_string()) + }); + rule!( #global : "*.*" | #db : ".*" | #table : ".
" + | #masking_policy : "MASKING POLICY " ) .parse(i) } @@ -3980,6 +4022,7 @@ pub fn grant_ownership_level(i: Input) -> IResult { Warehouse, Connection, Sequence, + MaskingPolicy, } let object = alt(( value(Object::Udf, rule! { UDF }), @@ -3987,6 +4030,7 @@ pub fn grant_ownership_level(i: Input) -> IResult { value(Object::Warehouse, rule! { WAREHOUSE }), value(Object::Connection, rule! { CONNECTION }), value(Object::Sequence, rule! { SEQUENCE }), + value(Object::MaskingPolicy, rule! { MASKING ~ POLICY }), )); // Object object_name @@ -3998,6 +4042,7 @@ pub fn grant_ownership_level(i: Input) -> IResult { Object::Warehouse => AccountMgrLevel::Warehouse(object_name.to_string()), Object::Connection => AccountMgrLevel::Connection(object_name.to_string()), Object::Sequence => AccountMgrLevel::Sequence(object_name.to_string()), + Object::MaskingPolicy => AccountMgrLevel::MaskingPolicy(object_name.to_string()), }, ); @@ -4046,7 +4091,7 @@ pub fn show_grant_option(i: Input) -> IResult { rule!( #grant_role: "FOR { ROLE | [USER] }" - | #share_object_name: "ON {DATABASE | TABLE . | UDF | STAGE | CONNECTION | SEQUENCE }" + | #share_object_name: "ON {DATABASE | TABLE . | UDF | STAGE | CONNECTION | SEQUENCE | MASKING POLICY }" | #role_granted: "OF ROLE " ).parse(i) } diff --git a/src/query/ast/src/parser/token.rs b/src/query/ast/src/parser/token.rs index 074fdb09bebf7..10c8045cd769f 100644 --- a/src/query/ast/src/parser/token.rs +++ b/src/query/ast/src/parser/token.rs @@ -351,6 +351,8 @@ pub enum TokenKind { ANY, #[token("APPEND_ONLY", ignore(ascii_case))] APPEND_ONLY, + #[token("APPLY", ignore(ascii_case))] + APPLY, #[token("ARGS", ignore(ascii_case))] ARGS, #[token("AUTO", ignore(ascii_case))] diff --git a/src/query/ast/tests/it/parser.rs b/src/query/ast/tests/it/parser.rs index 7c55d32edccbd..7188da3c12287 100644 --- a/src/query/ast/tests/it/parser.rs +++ b/src/query/ast/tests/it/parser.rs @@ -355,6 +355,7 @@ SELECT * from s;"#, r#"GRANT access connection on connection c1 TO 'test-grant';"#, r#"GRANT all on connection c1 TO 'test-grant';"#, r#"GRANT OWNERSHIP on connection c1 TO role r1;"#, + r#"GRANT OWNERSHIP on masking policy m1 TO role r1;"#, r#"GRANT access sequence, create sequence ON *.* TO 'test-grant';"#, r#"GRANT access sequence on sequence s1 TO 'test-grant';"#, r#"GRANT all on sequence s1 TO 'test-grant';"#, @@ -378,6 +379,11 @@ SELECT * from s;"#, r#"GRANT SELECT ON db01.tb1 TO ROLE role1;"#, r#"GRANT SELECT ON tb1 TO ROLE role1;"#, r#"GRANT ALL ON tb1 TO 'u1';"#, + r#"GRANT CREATE MASKING POLICY ON *.* TO USER a;"#, + r#"GRANT APPLY MASKING POLICY ON *.* TO USER a;"#, + r#"GRANT APPLY ON MASKING POLICY ssn_mask TO ROLE human_resources;"#, + r#"GRANT OWNERSHIP ON MASKING POLICY mask_phone TO ROLE role_mask_apply;"#, + r#"SHOW GRANTS ON MASKING POLICY ssn_mask;"#, r#"SHOW GRANTS;"#, r#"SHOW GRANTS FOR 'test-grant';"#, r#"SHOW GRANTS FOR USER 'test-grant';"#, diff --git a/src/query/ast/tests/it/testdata/stmt-error.txt b/src/query/ast/tests/it/testdata/stmt-error.txt index 72627c0cdc306..4e83cfee0e682 100644 --- a/src/query/ast/tests/it/testdata/stmt-error.txt +++ b/src/query/ast/tests/it/testdata/stmt-error.txt @@ -413,7 +413,7 @@ error: --> SQL:1:15 | 1 | GRANT SELECT, ALL PRIVILEGES, CREATE ON * TO 'test-grant'; - | ----- ------ ^^^ unexpected `ALL`, expecting `ALTER`, `SELECT`, `DELETE`, `ACCESS`, `USAGE`, `INSERT`, `UPDATE`, `SUPER`, `CREATE`, `DROP`, `GRANT`, or `SET` + | ----- ------ ^^^ unexpected `ALL`, expecting `ALTER`, `APPLY`, `SELECT`, `DELETE`, `ACCESS`, `USAGE`, `INSERT`, `UPDATE`, `SUPER`, `CREATE`, `DROP`, `GRANT`, or `SET` | | | | | while parsing ON | while parsing `GRANT { ROLE | schemaObjectPrivileges | ALL [ PRIVILEGES ] ON } TO { [ROLE ] | [USER] }` @@ -438,7 +438,7 @@ error: --> SQL:1:17 | 1 | GRANT select ON UDF a TO 'test-grant'; - | ----- ------ ^^^ unexpected `UDF`, expecting `IDENTIFIER`, , `FALSE`, `*`, , , , , or `TRUE` + | ----- ------ ^^^ unexpected `UDF`, expecting `IDENTIFIER`, , `FALSE`, `*`, , , , , `TRUE`, or `MASKING` | | | | | while parsing ON | while parsing `GRANT { ROLE | schemaObjectPrivileges | ALL [ PRIVILEGES ] ON } TO { [ROLE ] | [USER] }` @@ -451,7 +451,7 @@ error: --> SQL:1:24 | 1 | REVOKE SELECT, CREATE, ALL PRIVILEGES ON * FROM 'test-grant'; - | ------ ------ ^^^ unexpected `ALL`, expecting `ALTER`, `SELECT`, `DELETE`, `ACCESS`, `USAGE`, `INSERT`, `UPDATE`, `SUPER`, `CREATE`, `DROP`, `GRANT`, or `SET` + | ------ ------ ^^^ unexpected `ALL`, expecting `ALTER`, `APPLY`, `SELECT`, `DELETE`, `ACCESS`, `USAGE`, `INSERT`, `UPDATE`, `SUPER`, `CREATE`, `DROP`, `GRANT`, or `SET` | | | | | while parsing ON | while parsing `REVOKE { ROLE | schemaObjectPrivileges | ALL [ PRIVILEGES ] ON } FROM { [ROLE ] | [USER] }` @@ -988,7 +988,7 @@ error: --> SQL:1:8 | 1 | REVOKE OWNERSHIP, SELECT ON d20_0014.* FROM ROLE 'd20_0015_owner'; - | ------ ^^^^^^^^^ unexpected `OWNERSHIP`, expecting `INSERT`, `ALTER`, `SUPER`, `ROLE`, `ACCESS`, `WRITE`, `SET`, `SELECT`, `UPDATE`, `DELETE`, `DROP`, `READ`, `USAGE`, `GRANT`, `CREATE`, or `ALL` + | ------ ^^^^^^^^^ unexpected `OWNERSHIP`, expecting `INSERT`, `ALTER`, `SUPER`, `ROLE`, `ACCESS`, `WRITE`, `SET`, `SELECT`, `UPDATE`, `DELETE`, `DROP`, `READ`, `USAGE`, `GRANT`, `CREATE`, `ALL`, or `APPLY` | | | while parsing `REVOKE { ROLE | schemaObjectPrivileges | ALL [ PRIVILEGES ] ON } FROM { [ROLE ] | [USER] }` @@ -1000,7 +1000,7 @@ error: --> SQL:1:8 | 1 | REVOKE OWNERSHIP ON d20_0014.* FROM USER A; - | ------ ^^^^^^^^^ unexpected `OWNERSHIP`, expecting `INSERT`, `ALTER`, `SUPER`, `ROLE`, `ACCESS`, `WRITE`, `SET`, `SELECT`, `UPDATE`, `DELETE`, `DROP`, `READ`, `USAGE`, `GRANT`, `CREATE`, or `ALL` + | ------ ^^^^^^^^^ unexpected `OWNERSHIP`, expecting `INSERT`, `ALTER`, `SUPER`, `ROLE`, `ACCESS`, `WRITE`, `SET`, `SELECT`, `UPDATE`, `DELETE`, `DROP`, `READ`, `USAGE`, `GRANT`, `CREATE`, `ALL`, or `APPLY` | | | while parsing `REVOKE { ROLE | schemaObjectPrivileges | ALL [ PRIVILEGES ] ON } FROM { [ROLE ] | [USER] }` @@ -1012,7 +1012,7 @@ error: --> SQL:1:8 | 1 | REVOKE OWNERSHIP ON d20_0014.* FROM ROLE A; - | ------ ^^^^^^^^^ unexpected `OWNERSHIP`, expecting `INSERT`, `ALTER`, `SUPER`, `ROLE`, `ACCESS`, `WRITE`, `SET`, `SELECT`, `UPDATE`, `DELETE`, `DROP`, `READ`, `USAGE`, `GRANT`, `CREATE`, or `ALL` + | ------ ^^^^^^^^^ unexpected `OWNERSHIP`, expecting `INSERT`, `ALTER`, `SUPER`, `ROLE`, `ACCESS`, `WRITE`, `SET`, `SELECT`, `UPDATE`, `DELETE`, `DROP`, `READ`, `USAGE`, `GRANT`, `CREATE`, `ALL`, or `APPLY` | | | while parsing `REVOKE { ROLE | schemaObjectPrivileges | ALL [ PRIVILEGES ] ON } FROM { [ROLE ] | [USER] }` @@ -1113,7 +1113,7 @@ error: --> SQL:1:16 | 1 | SHOW GRANTS ON task t1; - | ^^^^ unexpected `task`, expecting `STAGE`, `TABLE`, `SEQUENCE`, `WAREHOUSE`, `DATABASE`, `UDF`, `CONNECTION`, or `PROCEDURE` + | ^^^^ unexpected `task`, expecting `MASKING`, `STAGE`, `TABLE`, `SEQUENCE`, `WAREHOUSE`, `DATABASE`, `UDF`, `CONNECTION`, or `PROCEDURE` ---------- Input ---------- diff --git a/src/query/ast/tests/it/testdata/stmt.txt b/src/query/ast/tests/it/testdata/stmt.txt index c4927dd5e9411..eeb73b1348baa 100644 --- a/src/query/ast/tests/it/testdata/stmt.txt +++ b/src/query/ast/tests/it/testdata/stmt.txt @@ -16302,6 +16302,28 @@ Grant( ) +---------- Input ---------- +GRANT OWNERSHIP on masking policy m1 TO role r1; +---------- Output --------- +GRANT OWNERSHIP ON MASKING POLICY m1 TO ROLE 'r1' +---------- AST ------------ +Grant( + GrantStmt { + source: Privs { + privileges: [ + Ownership, + ], + level: MaskingPolicy( + "m1", + ), + }, + principal: Role( + "r1", + ), + }, +) + + ---------- Input ---------- GRANT access sequence, create sequence ON *.* TO 'test-grant'; ---------- Output --------- @@ -16837,6 +16859,116 @@ Grant( ) +---------- Input ---------- +GRANT CREATE MASKING POLICY ON *.* TO USER a; +---------- Output --------- +GRANT CREATE MASKING POLICY ON *.* TO USER 'a'@'%' +---------- AST ------------ +Grant( + GrantStmt { + source: Privs { + privileges: [ + CreateMaskingPolicy, + ], + level: Global, + }, + principal: User( + UserIdentity { + username: "a", + hostname: "%", + }, + ), + }, +) + + +---------- Input ---------- +GRANT APPLY MASKING POLICY ON *.* TO USER a; +---------- Output --------- +GRANT APPLY MASKING POLICY ON *.* TO USER 'a'@'%' +---------- AST ------------ +Grant( + GrantStmt { + source: Privs { + privileges: [ + ApplyMaskingPolicy, + ], + level: Global, + }, + principal: User( + UserIdentity { + username: "a", + hostname: "%", + }, + ), + }, +) + + +---------- Input ---------- +GRANT APPLY ON MASKING POLICY ssn_mask TO ROLE human_resources; +---------- Output --------- +GRANT APPLY ON MASKING POLICY ssn_mask TO ROLE 'human_resources' +---------- AST ------------ +Grant( + GrantStmt { + source: Privs { + privileges: [ + ApplyMaskingPolicy, + ], + level: MaskingPolicy( + "ssn_mask", + ), + }, + principal: Role( + "human_resources", + ), + }, +) + + +---------- Input ---------- +GRANT OWNERSHIP ON MASKING POLICY mask_phone TO ROLE role_mask_apply; +---------- Output --------- +GRANT OWNERSHIP ON MASKING POLICY mask_phone TO ROLE 'role_mask_apply' +---------- AST ------------ +Grant( + GrantStmt { + source: Privs { + privileges: [ + Ownership, + ], + level: MaskingPolicy( + "mask_phone", + ), + }, + principal: Role( + "role_mask_apply", + ), + }, +) + + +---------- Input ---------- +SHOW GRANTS ON MASKING POLICY ssn_mask; +---------- Output --------- +SHOW GRANTS ON MASKING POLICY ssn_mask +---------- AST ------------ +ShowObjectPrivileges( + ShowObjectPrivilegesStmt { + object: MaskingPolicy( + "ssn_mask", + ), + show_option: Some( + ShowOptions { + show_limit: None, + limit: None, + }, + ), + }, +) + + ---------- Input ---------- SHOW GRANTS; ---------- Output --------- @@ -19452,7 +19584,7 @@ ShowObjectPrivileges( ---------- Input ---------- SHOW GRANTS ON CONNECTION c1; ---------- Output --------- -SHOW GRANTS ON CONNECTION c1 +SHOW GRANTS ON CONNECTION c1 ---------- AST ------------ ShowObjectPrivileges( ShowObjectPrivilegesStmt { diff --git a/src/query/ee/src/data_mask/data_mask_handler.rs b/src/query/ee/src/data_mask/data_mask_handler.rs index c6f9356f8a9cd..a1052154b8acc 100644 --- a/src/query/ee/src/data_mask/data_mask_handler.rs +++ b/src/query/ee/src/data_mask/data_mask_handler.rs @@ -47,17 +47,22 @@ impl DatamaskHandler for RealDatamaskHandler { meta_api.create_data_mask(req).await } - async fn drop_data_mask(&self, meta_api: Arc, req: DropDatamaskReq) -> Result<()> { + async fn drop_data_mask( + &self, + meta_api: Arc, + req: DropDatamaskReq, + ) -> Result> { let dropped = meta_api.drop_data_mask(&req.name).await??; - if dropped.is_none() { - if req.if_exists { - // Ok - } else { - return Err(AppError::from(req.name.unknown_error("drop data mask")).into()); + match dropped { + Some((seq_id, _)) => Ok(Some(*seq_id.data)), + None => { + if req.if_exists { + Ok(None) + } else { + Err(AppError::from(req.name.unknown_error("drop data mask")).into()) + } } } - - Ok(()) } async fn get_data_mask( diff --git a/src/query/ee_features/data_mask/src/data_mask_handler.rs b/src/query/ee_features/data_mask/src/data_mask_handler.rs index 7e0b60f4cb297..c5369275cecce 100644 --- a/src/query/ee_features/data_mask/src/data_mask_handler.rs +++ b/src/query/ee_features/data_mask/src/data_mask_handler.rs @@ -52,7 +52,11 @@ pub trait DatamaskHandler: Sync + Send { MetaError, >; - async fn drop_data_mask(&self, meta_api: Arc, req: DropDatamaskReq) -> Result<()>; + async fn drop_data_mask( + &self, + meta_api: Arc, + req: DropDatamaskReq, + ) -> Result>; async fn get_data_mask( &self, @@ -93,7 +97,7 @@ impl DatamaskHandlerWrapper { &self, meta_api: Arc, req: DropDatamaskReq, - ) -> Result<()> { + ) -> Result> { self.handler.drop_data_mask(meta_api, req).await } diff --git a/src/query/management/src/role/role_mgr.rs b/src/query/management/src/role/role_mgr.rs index c6793698e45df..4b657be07cf21 100644 --- a/src/query/management/src/role/role_mgr.rs +++ b/src/query/management/src/role/role_mgr.rs @@ -669,6 +669,7 @@ fn convert_to_grant_obj(owner_obj: &OwnershipObject) -> GrantObject { OwnershipObject::Connection { name } => GrantObject::Connection(name.to_string()), OwnershipObject::Sequence { name } => GrantObject::Sequence(name.to_string()), OwnershipObject::Procedure { procedure_id } => GrantObject::Procedure(*procedure_id), + OwnershipObject::MaskingPolicy { policy_id } => GrantObject::MaskingPolicy(*policy_id), } } diff --git a/src/query/service/src/interpreters/access/privilege_access.rs b/src/query/service/src/interpreters/access/privilege_access.rs index 8fd3c2ef44d60..6414bf8bd8833 100644 --- a/src/query/service/src/interpreters/access/privilege_access.rs +++ b/src/query/service/src/interpreters/access/privilege_access.rs @@ -25,6 +25,8 @@ use databend_common_exception::ErrorCode; use databend_common_exception::Result; use databend_common_management::RoleApi; use databend_common_management::WarehouseInfo; +use databend_common_meta_api::DatamaskApi; +use databend_common_meta_app::data_mask::DataMaskNameIdent; use databend_common_meta_app::principal::GetProcedureReq; use databend_common_meta_app::principal::GrantObject; use databend_common_meta_app::principal::OwnershipInfo; @@ -41,6 +43,7 @@ use databend_common_meta_app::tenant::Tenant; use databend_common_meta_types::SeqV; use databend_common_sql::binder::MutationType; use databend_common_sql::plans::InsertInputSource; +use databend_common_sql::plans::ModifyColumnAction; use databend_common_sql::plans::Mutation; use databend_common_sql::plans::OptimizeCompactBlock; use databend_common_sql::plans::PresignAction; @@ -169,6 +172,9 @@ impl PrivilegeAccess { GrantObject::Procedure(procedure_id) => OwnershipObject::Procedure { procedure_id: *procedure_id, }, + GrantObject::MaskingPolicy(policy_id) => OwnershipObject::MaskingPolicy { + policy_id: *policy_id, + }, GrantObject::Global => return Ok(None), }; @@ -662,7 +668,8 @@ impl PrivilegeAccess { | GrantObject::Connection(_) | GrantObject::Sequence(_) | GrantObject::Procedure(_) - | GrantObject::TableById(_, _, _) => true, + | GrantObject::TableById(_, _, _) + | GrantObject::MaskingPolicy(_) => true, GrantObject::Global => false, }; @@ -724,7 +731,8 @@ impl PrivilegeAccess { | GrantObject::Sequence(_) | GrantObject::Stage(_) | GrantObject::Database(_, _) - | GrantObject::Table(_, _, _) => Err(ErrorCode::PermissionDenied(format!( + | GrantObject::Table(_, _, _) + | GrantObject::MaskingPolicy(_) => Err(ErrorCode::PermissionDenied(format!( "Permission denied: privilege [{:?}] is required on {} for user {} with roles [{}]. \ Note: Please ensure that your current role have the appropriate permissions to create a new Object", privilege, @@ -776,6 +784,103 @@ impl PrivilegeAccess { .await } + async fn resolve_masking_policy_id_by_name(&self, policy_name: &str) -> Result { + let meta_api = UserApiProvider::instance().get_meta_store_client(); + let ident = DataMaskNameIdent::new(self.ctx.get_tenant(), policy_name); + if let Some(policy_id) = meta_api.get_data_mask_id(&ident).await? { + Ok(*policy_id.data) + } else { + Err(ErrorCode::UnknownDatamask(format!( + "Unknown masking policy {}", + policy_name + ))) + } + } + + async fn find_masking_policy_id_for_column( + &self, + catalog: &str, + database: &str, + table: &str, + column: &str, + ) -> Result> { + let tenant = self.ctx.get_tenant(); + let catalog = self.ctx.get_catalog(catalog).await?; + let table_obj = catalog.get_table(&tenant, database, table).await?; + let schema = table_obj.schema(); + if let Some((_, field)) = schema.column_with_name(column) { + if let Some(policy) = table_obj + .get_table_info() + .meta + .column_mask_policy_columns_ids + .get(&field.column_id) + { + return Ok(Some(policy.policy_id)); + } + } + Ok(None) + } + + async fn validate_masking_policy_access( + &self, + policy_id: u64, + policy_name: &str, + ) -> Result<()> { + match self + .validate_access( + &GrantObject::Global, + UserPrivilegeType::ApplyMaskingPolicy, + false, + false, + ) + .await + { + Ok(_) => return Ok(()), + Err(err) => { + if err.code() != ErrorCode::PERMISSION_DENIED { + return Err(err); + } + } + } + + match self + .validate_access( + &GrantObject::MaskingPolicy(policy_id), + UserPrivilegeType::ApplyMaskingPolicy, + false, + false, + ) + .await + { + Ok(_) => return Ok(()), + Err(err) => { + if err.code() != ErrorCode::PERMISSION_DENIED { + return Err(err); + } + } + } + + let session = self.ctx.get_current_session(); + if self + .has_ownership( + &session, + &GrantObject::MaskingPolicy(policy_id), + false, + false, + ) + .await? + { + return Ok(()); + } + + let current_user = self.ctx.get_current_user()?; + Err(ErrorCode::PermissionDenied(format!( + "Permission denied: APPLY MASKING POLICY or OWNERSHIP is required on MASKING POLICY {} for user {}", + policy_name, + current_user.identity().display() + ))) + } + async fn validate_udf_access(&self, udf_names: HashSet<&String>) -> Result<()> { // Note: validate_udf_access is not used for validate Create UDF for udf in udf_names { @@ -1221,7 +1326,41 @@ impl AccessChecker for PrivilegeAccess { self.validate_table_access(&plan.catalog, &plan.database, &plan.table, UserPrivilegeType::Alter, false, false).await? } Plan::ModifyTableColumn(plan) => { - self.validate_table_access(&plan.catalog, &plan.database, &plan.table, UserPrivilegeType::Alter, false, false).await? + self.validate_table_access( + &plan.catalog, + &plan.database, + &plan.table, + UserPrivilegeType::Alter, + false, + false, + ) + .await?; + + match &plan.action { + ModifyColumnAction::SetMaskingPolicy(policy_name, _) => { + let policy_id = self + .resolve_masking_policy_id_by_name(policy_name) + .await?; + self.validate_masking_policy_access(policy_id, policy_name) + .await?; + } + ModifyColumnAction::UnsetMaskingPolicy(column) => { + if let Some(policy_id) = self + .find_masking_policy_id_for_column( + &plan.catalog, + &plan.database, + &plan.table, + column, + ) + .await? + { + let policy_display = policy_id.to_string(); + self.validate_masking_policy_access(policy_id, &policy_display) + .await?; + } + } + _ => {} + } } Plan::ModifyTableComment(plan) => { self.validate_table_access(&plan.catalog, &plan.database, &plan.table, UserPrivilegeType::Alter, false, false).await? @@ -1581,14 +1720,36 @@ impl AccessChecker for PrivilegeAccess { self.validate_access(&GrantObject::Global, UserPrivilegeType::Super, false, false) .await?; } - Plan::CreateDatamaskPolicy(_) | Plan::DropDatamaskPolicy(_) => { - self.validate_access( - &GrantObject::Global, - UserPrivilegeType::CreateDataMask, - false, false, - ) + Plan::CreateDatamaskPolicy(_) => { + self + .validate_access( + &GrantObject::Global, + UserPrivilegeType::CreateMaskingPolicy, + false, + false, + ) .await?; } + Plan::DropDatamaskPolicy(plan) => { + if self + .validate_access(&GrantObject::Global, UserPrivilegeType::Super, false, false) + .await + .is_err() + { + match self + .resolve_masking_policy_id_by_name(&plan.name) + .await + { + Ok(policy_id) => { + self.validate_masking_policy_access(policy_id, &plan.name) + .await?; + } + Err(err) + if err.code() == ErrorCode::UNKNOWN_DATAMASK && plan.if_exists => {} + Err(err) => return Err(err), + } + } + } // Note: No need to check privileges // SET ROLE & SHOW ROLES is a session-local statement (have same semantic with the SET ROLE in postgres), no need to check privileges Plan::SetRole(_) => {} @@ -1604,7 +1765,13 @@ impl AccessChecker for PrivilegeAccess { Plan::ExplainSyntax { .. } => {} // just used in clickhouse-sqlalchemy, no need to check Plan::ExistsTable(_) => {} - Plan::DescDatamaskPolicy(_) => {} + Plan::DescDatamaskPolicy(plan) => { + let policy_id = self + .resolve_masking_policy_id_by_name(&plan.name) + .await?; + self.validate_masking_policy_access(policy_id, &plan.name) + .await?; + } Plan::Begin => {} Plan::CreateProcedure(_) => { if self @@ -1748,7 +1915,8 @@ fn check_db_tb_ownership_access( | OwnershipObject::Warehouse { .. } | OwnershipObject::Connection { .. } | OwnershipObject::Procedure { .. } - | OwnershipObject::Sequence { .. } => {} + | OwnershipObject::Sequence { .. } + | OwnershipObject::MaskingPolicy { .. } => {} } } } diff --git a/src/query/service/src/interpreters/common/grant.rs b/src/query/service/src/interpreters/common/grant.rs index cd62cee550bd4..61e67674af5ac 100644 --- a/src/query/service/src/interpreters/common/grant.rs +++ b/src/query/service/src/interpreters/common/grant.rs @@ -18,6 +18,7 @@ use databend_common_base::base::GlobalInstance; use databend_common_catalog::table_context::TableContext; use databend_common_exception::Result; use databend_common_management::WarehouseInfo; +use databend_common_meta_api::DatamaskApi; use databend_common_meta_app::principal::GrantObject; use databend_common_meta_app::schema::GetSequenceReq; use databend_common_meta_app::schema::SequenceIdent; @@ -146,6 +147,18 @@ pub async fn validate_grant_object_exists( )); } } + GrantObject::MaskingPolicy(policy_id) => { + let meta_api = UserApiProvider::instance().get_meta_store_client(); + if meta_api + .get_data_mask_by_id(&tenant, *policy_id) + .await? + .is_none() + { + return Err(databend_common_exception::ErrorCode::UnknownDatamask( + format!("masking policy id {} not exists", policy_id), + )); + } + } GrantObject::Global => (), } diff --git a/src/query/service/src/interpreters/interpreter_data_mask_create.rs b/src/query/service/src/interpreters/interpreter_data_mask_create.rs index 6116fefff9c65..206aef9877b74 100644 --- a/src/query/service/src/interpreters/interpreter_data_mask_create.rs +++ b/src/query/service/src/interpreters/interpreter_data_mask_create.rs @@ -18,7 +18,10 @@ use databend_common_exception::ErrorCode; use databend_common_exception::Result; use databend_common_license::license::Feature; use databend_common_license::license_manager::LicenseManagerSwitch; +use databend_common_management::RoleApi; +use databend_common_meta_app::principal::OwnershipObject; use databend_common_sql::plans::CreateDatamaskPolicyPlan; +use databend_common_users::RoleCacheManager; use databend_common_users::UserApiProvider; use databend_enterprise_data_mask_feature::get_datamask_handler; @@ -54,20 +57,37 @@ impl Interpreter for CreateDataMaskInterpreter { .check_enterprise_enabled(self.ctx.get_license_key(), Feature::DataMask)?; let meta_api = UserApiProvider::instance().get_meta_store_client(); let handler = get_datamask_handler(); - if let Err(_e) = handler + let tenant = self.plan.tenant.clone(); + + match handler .create_data_mask(meta_api, self.plan.clone().into()) .await? { - return if self.plan.if_not_exists { + Ok(reply) => { + if let Some(current_role) = self.ctx.get_current_role() { + let role_api = UserApiProvider::instance().role_api(&tenant); + role_api + .grant_ownership( + &OwnershipObject::MaskingPolicy { + policy_id: reply.id, + }, + ¤t_role.name, + ) + .await?; + RoleCacheManager::instance().invalidate_cache(&tenant); + } Ok(PipelineBuildResult::create()) - } else { - Err(ErrorCode::DatamaskAlreadyExists(format!( - "Security policy with name '{}' already exists", - self.plan.name - ))) - }; + } + Err(_e) => { + if self.plan.if_not_exists { + Ok(PipelineBuildResult::create()) + } else { + Err(ErrorCode::DatamaskAlreadyExists(format!( + "Security policy with name '{}' already exists", + self.plan.name + ))) + } + } } - - Ok(PipelineBuildResult::create()) } } diff --git a/src/query/service/src/interpreters/interpreter_data_mask_drop.rs b/src/query/service/src/interpreters/interpreter_data_mask_drop.rs index 2306c7e578db9..bbe6c6b555079 100644 --- a/src/query/service/src/interpreters/interpreter_data_mask_drop.rs +++ b/src/query/service/src/interpreters/interpreter_data_mask_drop.rs @@ -17,7 +17,10 @@ use std::sync::Arc; use databend_common_exception::Result; use databend_common_license::license::Feature; use databend_common_license::license_manager::LicenseManagerSwitch; +use databend_common_management::RoleApi; +use databend_common_meta_app::principal::OwnershipObject; use databend_common_sql::plans::DropDatamaskPolicyPlan; +use databend_common_users::RoleCacheManager; use databend_common_users::UserApiProvider; use databend_enterprise_data_mask_feature::get_datamask_handler; @@ -53,9 +56,17 @@ impl Interpreter for DropDataMaskInterpreter { .check_enterprise_enabled(self.ctx.get_license_key(), Feature::DataMask)?; let meta_api = UserApiProvider::instance().get_meta_store_client(); let handler = get_datamask_handler(); - handler - .drop_data_mask(meta_api, self.plan.clone().into()) - .await?; + let tenant = self.plan.tenant.clone(); + if let Some(policy_id) = handler + .drop_data_mask(meta_api.clone(), self.plan.clone().into()) + .await? + { + let role_api = UserApiProvider::instance().role_api(&tenant); + role_api + .revoke_ownership(&OwnershipObject::MaskingPolicy { policy_id }) + .await?; + RoleCacheManager::instance().invalidate_cache(&tenant); + } Ok(PipelineBuildResult::create()) } diff --git a/src/query/service/src/interpreters/interpreter_privilege_grant.rs b/src/query/service/src/interpreters/interpreter_privilege_grant.rs index 3cf0b3b913270..fc817198823e8 100644 --- a/src/query/service/src/interpreters/interpreter_privilege_grant.rs +++ b/src/query/service/src/interpreters/interpreter_privilege_grant.rs @@ -135,6 +135,9 @@ impl GrantPrivilegeInterpreter { GrantObject::Procedure(p) => Ok(OwnershipObject::Procedure { procedure_id: *p, }), + GrantObject::MaskingPolicy(policy_id) => Ok(OwnershipObject::MaskingPolicy { + policy_id: *policy_id, + }), GrantObject::Global => Err(ErrorCode::IllegalGrant( "Illegal GRANT/REVOKE command; please consult the manual to see which privileges can be used", )), @@ -248,7 +251,7 @@ impl Interpreter for GrantPrivilegeInterpreter { self.grant_ownership(&self.ctx, &tenant, &owner_object, &role) .await?; } else { - return Err(databend_common_exception::ErrorCode::UnknownRole( + return Err(ErrorCode::UnknownRole( "No current role, cannot grant ownership", )); } diff --git a/src/query/service/src/table_functions/show_grants/show_grants_table.rs b/src/query/service/src/table_functions/show_grants/show_grants_table.rs index fe428248e1228..5ccfcb1bd6b8a 100644 --- a/src/query/service/src/table_functions/show_grants/show_grants_table.rs +++ b/src/query/service/src/table_functions/show_grants/show_grants_table.rs @@ -39,6 +39,7 @@ use databend_common_expression::TableSchemaRefExt; use databend_common_management::RoleApi; use databend_common_management::UserApi; use databend_common_management::WarehouseInfo; +use databend_common_meta_api::DatamaskApi; use databend_common_meta_app::principal::GrantEntry; use databend_common_meta_app::principal::GrantObject; use databend_common_meta_app::principal::OwnershipObject; @@ -247,7 +248,7 @@ impl AsyncSource for ShowGrantsSource { show_account_grants(self.ctx.clone(), &self.grant_type, &self.name).await? } "table" | "database" | "udf" | "stage" | "warehouse" | "connection" | "sequence" - | "procedure" => { + | "procedure" | "masking_policy" => { show_object_grant( self.ctx.clone(), &self.grant_type, @@ -260,7 +261,7 @@ impl AsyncSource for ShowGrantsSource { "role_grantee" => show_role_grantees(self.ctx.clone(), &self.name).await?, _ => { return Err(ErrorCode::InvalidArgument(format!( - "Expected 'user|role|table|database|udf|stage|warehouse|connection|sequence|procedure|role_grantee', but got {:?}", + "Expected 'user|role|table|database|udf|stage|warehouse|connection|sequence|procedure|masking_policy|role_grantee', but got {:?}", self.grant_type ))); } @@ -564,6 +565,18 @@ async fn show_account_grants( grant_list.push(format!("{} TO {}", grant_entry, identity)); } } + GrantObject::MaskingPolicy(policy_id) => { + let meta_api = UserApiProvider::instance().get_meta_store_client(); + if let Some(policy_name) = meta_api + .get_data_mask_name_by_id(&ctx.get_tenant(), *policy_id) + .await? + { + object_name.push(policy_name); + object_id.push(Some(policy_id.to_string())); + privileges.push(get_priv_str(&grant_entry)); + grant_list.push(format!("{} TO {}", grant_entry, identity)); + } + } GrantObject::Global => { // grant all on *.* to a object_name.push("*.*".to_string()); @@ -679,12 +692,27 @@ async fn show_account_grants( if let Some(p) = procedure_api.get_procedure_name_by_id(procedure_id).await? { - object_name.push(p); + object_name.push(p.to_string()); object_id.push(Some(procedure_id.to_string())); privileges.push("OWNERSHIP".to_string()); grant_list.push(format!( "GRANT OWNERSHIP ON PROCEDURE {} TO {}", - procedure_id, identity + p, identity + )); + } + } + OwnershipObject::MaskingPolicy { policy_id } => { + let meta_api = UserApiProvider::instance().get_meta_store_client(); + if let Some(policy_name) = meta_api + .get_data_mask_name_by_id(&ctx.get_tenant(), policy_id) + .await? + { + object_name.push(policy_name.clone()); + object_id.push(Some(policy_id.to_string())); + privileges.push("OWNERSHIP".to_string()); + grant_list.push(format!( + "GRANT OWNERSHIP ON MASKING POLICY {} TO {}", + policy_name, identity )); } } @@ -972,9 +1000,39 @@ async fn show_object_grant( ))); } } + "masking_policy" => { + let policy_id = name.parse::()?; + if !visibility_checker.check_masking_policy_visibility(&policy_id) { + return Err(ErrorCode::PermissionDenied(format!( + "Permission denied: privilege APPLY MASKING POLICY or OWNERSHIP is required on masking policy {} for user {}", + name, current_user + ))); + } + let meta_api = UserApiProvider::instance().get_meta_store_client(); + if let Some(policy_name) = meta_api + .get_data_mask_name_by_id(&ctx.get_tenant(), policy_id) + .await? + { + ( + GrantObject::MaskingPolicy(policy_id), + OwnershipObject::MaskingPolicy { policy_id }, + Some(policy_id.to_string()), + policy_name, + ) + } else { + ( + // Already get the policy id but old version policy does not have key DataMaskIdToNameIdent + // directly return policy id as name. Extremely low probability event + GrantObject::MaskingPolicy(policy_id), + OwnershipObject::MaskingPolicy { policy_id }, + Some(policy_id.to_string()), + policy_id.to_string(), + ) + } + } _ => { return Err(ErrorCode::InvalidArgument(format!( - "Expected 'table|database|udf|stage|warehouse|connection|sequence|procedure', but got {:?}", + "Expected 'table|database|udf|stage|warehouse|connection|sequence|procedure|masking_policy', but got {:?}", grant_type ))); } @@ -992,12 +1050,13 @@ async fn show_object_grant( } } - let ownerships = user_api.role_api(&tenant).list_ownerships().await?; - for ownership in ownerships { - if ownership.data.object == owner_object { - privileges.push("OWNERSHIP".to_string()); - names.push(ownership.data.role); - } + if let Some(ownership) = user_api + .role_api(&tenant) + .get_ownership(&owner_object) + .await? + { + privileges.push("OWNERSHIP".to_string()); + names.push(ownership.role); } let object_ids = vec![object_id; privileges.len()]; diff --git a/src/query/sql/Cargo.toml b/src/query/sql/Cargo.toml index d4439203bb2c8..adc38cf9aa38f 100644 --- a/src/query/sql/Cargo.toml +++ b/src/query/sql/Cargo.toml @@ -21,6 +21,7 @@ databend-common-expression = { workspace = true } databend-common-functions = { workspace = true } databend-common-license = { workspace = true } databend-common-management = { workspace = true } +databend-common-meta-api = { workspace = true } databend-common-meta-app = { workspace = true } databend-common-meta-types = { workspace = true } databend-common-metrics = { workspace = true } diff --git a/src/query/sql/src/planner/binder/ddl/account.rs b/src/query/sql/src/planner/binder/ddl/account.rs index 4a170bad1aeac..287cd40607241 100644 --- a/src/query/sql/src/planner/binder/ddl/account.rs +++ b/src/query/sql/src/planner/binder/ddl/account.rs @@ -31,6 +31,8 @@ use databend_common_base::base::GlobalInstance; use databend_common_exception::ErrorCode; use databend_common_exception::Result; use databend_common_management::WorkloadMgr; +use databend_common_meta_api::data_mask_api::DatamaskApi; +use databend_common_meta_app::data_mask::DataMaskNameIdent; use databend_common_meta_app::principal::AuthInfo; use databend_common_meta_app::principal::GetProcedureReq; use databend_common_meta_app::principal::GrantObject; @@ -220,11 +222,16 @@ impl Binder { ))) } } + AccountMgrLevel::MaskingPolicy(policy) => { + let policy_id = self.resolve_masking_policy_id(policy).await?; + Ok(GrantObject::MaskingPolicy(policy_id)) + } } } // Some old query version use GrantObject::Table store table name. // So revoke need compat the old version. + #[async_backtrace::framed] pub(in crate::planner::binder) async fn convert_to_revoke_grant_object( &self, source: &AccountMgrLevel, @@ -303,6 +310,23 @@ impl Binder { ))) } } + AccountMgrLevel::MaskingPolicy(policy) => { + let policy_id = self.resolve_masking_policy_id(policy).await?; + Ok(vec![GrantObject::MaskingPolicy(policy_id)]) + } + } + } + + async fn resolve_masking_policy_id(&self, policy: &str) -> Result { + let meta_api = UserApiProvider::instance().get_meta_store_client(); + let ident = DataMaskNameIdent::new(self.ctx.get_tenant(), policy); + if let Some(policy_id) = meta_api.get_data_mask_id(&ident).await? { + Ok(*policy_id.data) + } else { + Err(ErrorCode::UnknownDatamask(format!( + "Unknown masking policy {}", + policy + ))) } } @@ -583,6 +607,13 @@ impl Binder { ))); } } + GrantObjectName::MaskingPolicy(policy) => { + let policy_id = self.resolve_masking_policy_id(policy).await?; + format!( + "SELECT * FROM show_grants('masking_policy', '{}')", + policy_id + ) + } }; let (show_limit, limit_str) = get_show_options(show_option, Some("name".to_string())); diff --git a/src/query/users/src/user_mgr.rs b/src/query/users/src/user_mgr.rs index b857b5f122beb..9536c5bb28d33 100644 --- a/src/query/users/src/user_mgr.rs +++ b/src/query/users/src/user_mgr.rs @@ -217,9 +217,10 @@ impl UserApiProvider { | GrantObject::Connection(_) | GrantObject::Sequence(_) | GrantObject::Procedure(_) + | GrantObject::MaskingPolicy(_) ) { return Err(ErrorCode::IllegalUser(format!( - "Cannot grant warehouse|connection|Sequence|Procedure privileges to user `{}`", + "Cannot grant Warehouse|Connection|Sequence|Procedure|MaskingPolicy privileges to user `{}`", user.username ))); } diff --git a/src/query/users/src/visibility_checker.rs b/src/query/users/src/visibility_checker.rs index 9e29314e8fc0a..70c2a5b9a7f1a 100644 --- a/src/query/users/src/visibility_checker.rs +++ b/src/query/users/src/visibility_checker.rs @@ -116,6 +116,7 @@ pub struct GrantObjectVisibilityChecker { granted_global_c: bool, granted_global_seq: bool, granted_global_procedure: bool, + granted_global_masking_policy: bool, granted_global_stage: bool, granted_global_read_stage: bool, granted_global_db_table: bool, @@ -129,6 +130,7 @@ pub struct GrantObjectVisibilityChecker { granted_tables_id: FastHashMap>>, extra_databases_id: FastHashMap>, granted_procedures_id: FastHashSet, + granted_masking_policies_id: FastHashSet, // Name-based storage (backward compatibility) granted_databases: FxHashSet<(Arc, Arc)>, @@ -155,6 +157,7 @@ impl GrantObjectVisibilityChecker { let mut granted_global_stage = false; let mut granted_global_read_stage = false; let mut granted_global_db_table = false; + let mut granted_global_masking_policy = false; let mut catalog_pool = CatalogIdPool::new(); let total_objects = ownership_objects.len(); // Most deployments use only the default catalog @@ -178,8 +181,8 @@ impl GrantObjectVisibilityChecker { FastHashMap::with_capacity(estimated_catalogs); let mut extra_databases_id: FastHashMap> = FastHashMap::with_capacity(estimated_catalogs); - let mut granted_procedures_id: FastHashSet = - FastHashSet::with_capacity(total_objects / 10); + let mut granted_procedures_id: FastHashSet = FastHashSet::with_capacity(16); + let mut granted_masking_policies_id: FastHashSet = FastHashSet::with_capacity(16); let mut granted_databases: FxHashSet<(Arc, Arc)> = FxHashSet::with_capacity_and_hasher(total_objects / 10, Default::default()); @@ -257,6 +260,15 @@ impl GrantObjectVisibilityChecker { }, ); + check_privilege( + &mut granted_global_masking_policy, + grant_entry.privileges().iter(), + |privilege| { + UserPrivilegeSet::available_privileges_on_masking_policy(false) + .has_privilege(privilege) + }, + ); + check_privilege( &mut granted_global_stage, grant_entry.privileges().iter(), @@ -366,6 +378,9 @@ impl GrantObjectVisibilityChecker { GrantObject::Procedure(procedure_id) => { let _ = granted_procedures_id.set_insert(*procedure_id); } + GrantObject::MaskingPolicy(policy_id) => { + let _ = granted_masking_policies_id.set_insert(*policy_id); + } } } } @@ -426,9 +441,12 @@ impl GrantObjectVisibilityChecker { OwnershipObject::Sequence { name } => { granted_seq.insert(Arc::from(name.as_str())); } - OwnershipObject::Procedure { procedure_id, .. } => { + OwnershipObject::Procedure { procedure_id } => { let _ = granted_procedures_id.set_insert(*procedure_id); } + OwnershipObject::MaskingPolicy { policy_id } => { + let _ = granted_masking_policies_id.set_insert(*policy_id); + } } } @@ -447,6 +465,7 @@ impl GrantObjectVisibilityChecker { granted_global_c, granted_global_seq, granted_global_procedure, + granted_global_masking_policy, granted_global_stage, granted_global_read_stage, granted_global_db_table, @@ -465,6 +484,7 @@ impl GrantObjectVisibilityChecker { granted_c, granted_seq, granted_procedures_id, + granted_masking_policies_id, } } @@ -688,6 +708,14 @@ impl GrantObjectVisibilityChecker { false } + #[inline(always)] + pub fn check_masking_policy_visibility(&self, id: &u64) -> bool { + if self.granted_global_masking_policy { + return true; + } + self.granted_masking_policies_id.contains(id) + } + #[allow(clippy::type_complexity)] pub fn get_visibility_database( &self, diff --git a/tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.result b/tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.result new file mode 100644 index 0000000000000..f865577c3ff20 --- /dev/null +++ b/tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.result @@ -0,0 +1,43 @@ +#### prepare masking policy rbac environment +>>>> DROP TABLE IF EXISTS mp_rbac +>>>> DROP MASKING POLICY IF EXISTS mask_phone +>>>> DROP MASKING POLICY IF EXISTS mask_email +>>>> DROP USER IF EXISTS mask_create +>>>> DROP USER IF EXISTS mask_apply +>>>> DROP USER IF EXISTS mask_desc +>>>> DROP ROLE IF EXISTS role_mask_apply +>>>> DROP ROLE IF EXISTS role_mask_create +>>>> CREATE TABLE mp_rbac(id INT, phone STRING, email STRING) +>>>> CREATE USER mask_create IDENTIFIED BY '123' with default_role='role_mask_create' +>>>> CREATE USER mask_apply IDENTIFIED BY '123' with default_role='role_mask_apply' +>>>> CREATE ROLE role_mask_apply +>>>> GRANT ROLE role_mask_apply TO mask_apply +>>>> CREATE ROLE role_mask_create +>>>> GRANT ROLE role_mask_create TO mask_create +>>>> CREATE USER mask_desc IDENTIFIED BY '123' with default_role='mask_desc' +#### create privilege requires CREATE MASKING POLICY +Error: APIError: QueryFailed: [1063]Permission denied: privilege [CreateMaskingPolicy] is required on *.* for user 'mask_create'@'%' with roles [public,role_mask_create]. Note: Please ensure that your current role have the appropriate permissions to create a new Object +>>>> GRANT CREATE MASKING POLICY ON *.* TO USER mask_create +>>>> CREATE MASKING POLICY mask_email AS (val STRING) RETURNS STRING -> 'EMAIL' +Error: APIError: QueryFailed: [1063]Permission denied: APPLY MASKING POLICY or OWNERSHIP is required on MASKING POLICY mask_phone for user 'mask_apply'@'%' +1 +#### apply privilege requires APPLY on masking policy +>>>> GRANT ALTER ON default.mp_rbac TO ROLE role_mask_apply +>>>> ALTER TABLE mp_rbac MODIFY COLUMN phone SET MASKING POLICY mask_phone +1 +>>>> GRANT APPLY ON MASKING POLICY mask_phone TO ROLE role_mask_apply +1 +>>>> GRANT OWNERSHIP ON MASKING POLICY mask_phone TO ROLE role_mask_apply +2 +Error: APIError: QueryFailed: [1063]Permission denied: APPLY MASKING POLICY or OWNERSHIP is required on MASKING POLICY mask_email for user 'mask_apply'@'%' +>>>> GRANT APPLY MASKING POLICY ON *.* TO ROLE role_mask_apply +2 +1 +#### cleanup masking policy rbac environment +>>>> DROP TABLE IF EXISTS mp_rbac +>>>> DROP MASKING POLICY IF EXISTS mask_phone +>>>> DROP MASKING POLICY IF EXISTS mask_email +>>>> DROP USER IF EXISTS mask_create +>>>> DROP USER IF EXISTS mask_apply +>>>> DROP USER IF EXISTS mask_desc +>>>> DROP ROLE IF EXISTS role_mask_apply diff --git a/tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.sh b/tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.sh new file mode 100755 index 0000000000000..d3232754dc5ea --- /dev/null +++ b/tests/suites/5_ee/10_rbac/10_0001_masking_policy_rbac.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +. "$CURDIR"/../../../shell_env.sh + +comment "prepare masking policy rbac environment" +stmt "DROP TABLE IF EXISTS mp_rbac" +stmt "DROP MASKING POLICY IF EXISTS mask_phone" +stmt "DROP MASKING POLICY IF EXISTS mask_email" +stmt "DROP USER IF EXISTS mask_create" +stmt "DROP USER IF EXISTS mask_apply" +stmt "DROP USER IF EXISTS mask_desc" +stmt "DROP ROLE IF EXISTS role_mask_apply" +stmt "DROP ROLE IF EXISTS role_mask_create" + +stmt "CREATE TABLE mp_rbac(id INT, phone STRING, email STRING)" +stmt "CREATE USER mask_create IDENTIFIED BY '123' with default_role='role_mask_create'" +stmt "CREATE USER mask_apply IDENTIFIED BY '123' with default_role='role_mask_apply'" +stmt "CREATE ROLE role_mask_apply" +stmt "GRANT ROLE role_mask_apply TO mask_apply" +stmt "CREATE ROLE role_mask_create" +stmt "GRANT ROLE role_mask_create TO mask_create" +stmt "CREATE USER mask_desc IDENTIFIED BY '123' with default_role='mask_desc'" + +export USER_MASK_DESC="bendsql --user=mask_desc --password=123 --host=${QUERY_MYSQL_HANDLER_HOST} --port ${QUERY_HTTP_HANDLER_PORT} --quote-style=never" + +export USER_MASK_CREATE="bendsql --user=mask_create --password=123 --host=${QUERY_MYSQL_HANDLER_HOST} --port ${QUERY_HTTP_HANDLER_PORT} --quote-style=never" +export USER_MASK_APPLY="bendsql --user=mask_apply --password=123 --host=${QUERY_MYSQL_HANDLER_HOST} --port ${QUERY_HTTP_HANDLER_PORT} --quote-style=never" + +comment "create privilege requires CREATE MASKING POLICY" +echo "CREATE MASKING POLICY mask_phone AS (val STRING) RETURNS STRING -> concat('***', right(val, 2));" | $USER_MASK_CREATE +stmt "GRANT CREATE MASKING POLICY ON *.* TO USER mask_create" +echo "CREATE MASKING POLICY mask_phone AS (val STRING) RETURNS STRING -> concat('***', right(val, 2));" | $USER_MASK_CREATE +stmt "CREATE MASKING POLICY mask_email AS (val STRING) RETURNS STRING -> 'EMAIL'" +#expect failure for user without privileges +echo "DESC MASKING POLICY mask_phone; " | $USER_MASK_APPLY +echo "DESC MASKING POLICY mask_phone;" | $USER_MASK_CREATE | grep mask_phone | wc -l + +comment "apply privilege requires APPLY on masking policy" +stmt "GRANT ALTER ON default.mp_rbac TO ROLE role_mask_apply" +stmt "ALTER TABLE mp_rbac MODIFY COLUMN phone SET MASKING POLICY mask_phone" +# expect failure +echo "ALTER TABLE mp_rbac MODIFY COLUMN phone UNSET MASKING POLICY; " | $USER_MASK_APPLY 2>&1 | grep 1063 |wc -l +stmt "GRANT APPLY ON MASKING POLICY mask_phone TO ROLE role_mask_apply" +echo "ALTER TABLE mp_rbac MODIFY COLUMN phone UNSET MASKING POLICY;" | $USER_MASK_APPLY +echo "ALTER TABLE mp_rbac MODIFY COLUMN phone SET MASKING POLICY mask_phone;" | $USER_MASK_APPLY +echo "DESC MASKING POLICY mask_phone;" | $USER_MASK_APPLY | grep mask_phone | wc -l +stmt "GRANT OWNERSHIP ON MASKING POLICY mask_phone TO ROLE role_mask_apply" +stmt "SHOW GRANTS ON MASKING POLICY mask_phone" | grep role_mask_apply |wc -l + +## expect failure +echo "ALTER TABLE mp_rbac MODIFY COLUMN email SET MASKING POLICY mask_email" | $USER_MASK_APPLY +stmt "GRANT APPLY MASKING POLICY ON *.* TO ROLE role_mask_apply" +echo "ALTER TABLE mp_rbac MODIFY COLUMN email SET MASKING POLICY mask_email;" | $USER_MASK_APPLY +stmt "SHOW GRANTS ON MASKING POLICY mask_email" | grep mask_email|wc -l +echo "DESC MASKING POLICY mask_email;" | $USER_MASK_APPLY | grep mask_email|wc -l + +comment "cleanup masking policy rbac environment" +stmt "DROP TABLE IF EXISTS mp_rbac" +stmt "DROP MASKING POLICY IF EXISTS mask_phone" +stmt "DROP MASKING POLICY IF EXISTS mask_email" +stmt "DROP USER IF EXISTS mask_create" +stmt "DROP USER IF EXISTS mask_apply" +stmt "DROP USER IF EXISTS mask_desc" +stmt "DROP ROLE IF EXISTS role_mask_apply" From d92fbc769aa1d08862f99614d0f38387ab302956 Mon Sep 17 00:00:00 2001 From: TCeason Date: Tue, 18 Nov 2025 21:02:12 +0800 Subject: [PATCH 2/3] add some comment --- src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs | 5 ++++- .../row_access_policy/row_access_policy_id_to_name_ident.rs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs b/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs index ee792c7420e9e..b6fa4d8fef77d 100644 --- a/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs +++ b/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs @@ -16,7 +16,10 @@ use crate::data_mask::DataMaskId; use crate::tenant_key::ident::TIdent; use crate::tenant_key::raw::TIdentRaw; +/// Tenantless key mapping a masking policy id back to its name. +/// This enables reverse lookup for SHOW GRANTS and ownership listings. pub type DataMaskIdToNameIdent = TIdent; +/// Raw form of [`DataMaskIdToNameIdent`] used for serde/protobuf. pub type DataMaskIdToNameIdentRaw = TIdentRaw; pub use kvapi_impl::Resource; @@ -45,7 +48,7 @@ mod kvapi_impl { impl TenantResource for Resource { const PREFIX: &'static str = "__fd_datamask_id_to_name"; const TYPE: &'static str = "DataMaskIdToNameIdent"; - const HAS_TENANT: bool = false; + const HAS_TENANT: bool = true; type ValueType = DataMaskNameIdentRaw; } diff --git a/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs b/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs index 56de59c1e3ea6..3b67a86b133a9 100644 --- a/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs +++ b/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs @@ -16,7 +16,9 @@ use crate::row_access_policy::RowAccessPolicyId; use crate::tenant_key::ident::TIdent; use crate::tenant_key::raw::TIdentRaw; +/// Tenantless key mapping a row access policy id back to its name for reverse lookups. pub type RowAccessPolicyIdToNameIdent = TIdent; +/// Raw form of [`RowAccessPolicyIdToNameIdent`] used for serde/protobuf. pub type RowAccessPolicyIdToNameIdentRaw = TIdentRaw; pub use kvapi_impl::Resource; @@ -45,7 +47,7 @@ mod kvapi_impl { impl TenantResource for Resource { const PREFIX: &'static str = "__fd_row_access_policy_id_to_name"; const TYPE: &'static str = "RowAccessPolicyIdToNameIdent"; - const HAS_TENANT: bool = false; + const HAS_TENANT: bool = true; type ValueType = RowAccessPolicyNameIdentRaw; } From 8fd446265ca0caed580fe2a54dffab311398c3f9 Mon Sep 17 00:00:00 2001 From: TCeason Date: Wed, 19 Nov 2025 11:01:34 +0800 Subject: [PATCH 3/3] add some unit test --- .../data_mask/data_mask_id_to_name_ident.rs | 20 ++++++++++++++++ .../row_access_policy_id_to_name_ident.rs | 23 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs b/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs index b6fa4d8fef77d..21b08697f574c 100644 --- a/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs +++ b/src/meta/app/src/data_mask/data_mask_id_to_name_ident.rs @@ -59,3 +59,23 @@ mod kvapi_impl { } } } + +#[cfg(test)] +mod tests { + use databend_common_meta_kvapi::kvapi::Key; + + use crate::data_mask::DataMaskId; + use crate::data_mask::DataMaskIdToNameIdent; + use crate::tenant::Tenant; + + #[test] + fn test_data_mask_id_ident() { + let tenant = Tenant::new_literal("dummy"); + let ident = DataMaskIdToNameIdent::new_generic(tenant, DataMaskId::new(3)); + + let key = ident.to_string_key(); + assert_eq!(key, "__fd_datamask_id_to_name/dummy/3"); + + assert_eq!(ident, DataMaskIdToNameIdent::from_str_key(&key).unwrap()); + } +} diff --git a/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs b/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs index 3b67a86b133a9..aa1568ec17a4c 100644 --- a/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs +++ b/src/meta/app/src/row_access_policy/row_access_policy_id_to_name_ident.rs @@ -58,3 +58,26 @@ mod kvapi_impl { } } } + +#[cfg(test)] +mod tests { + use databend_common_meta_kvapi::kvapi::Key; + + use crate::row_access_policy::RowAccessPolicyId; + use crate::row_access_policy::RowAccessPolicyIdToNameIdent; + use crate::tenant::Tenant; + + #[test] + fn test_row_access_policy_id_ident() { + let tenant = Tenant::new_literal("dummy"); + let ident = RowAccessPolicyIdToNameIdent::new_generic(tenant, RowAccessPolicyId::new(3)); + + let key = ident.to_string_key(); + assert_eq!(key, "__fd_row_access_policy_id_to_name/dummy/3"); + + assert_eq!( + ident, + RowAccessPolicyIdToNameIdent::from_str_key(&key).unwrap() + ); + } +}