diff --git a/pallets/tables/src/lib.rs b/pallets/tables/src/lib.rs index 326f9096..7a42f159 100644 --- a/pallets/tables/src/lib.rs +++ b/pallets/tables/src/lib.rs @@ -770,7 +770,8 @@ pub mod pallet { /// Emits `Event::QuorumUpdated`. /// /// # Permissions - /// Requires `TablesPalletPermission::EditSchema`. + /// Requires `TablesPalletPermission::EditSchema`, or the caller must be the current + /// owner of the table. #[pallet::call_index(9)] #[pallet::weight(::WeightInfo::update_table_quorum())] pub fn update_table_quorum( @@ -781,7 +782,8 @@ pub mod pallet { let _ = pallet_permissions::Pallet::::ensure_root_or_permissioned( origin.clone(), &PermissionLevel::TablesPallet(TablesPalletPermission::EditSchema), - )?; + ) + .or_else(|_| Self::ensure_root_or_owner(origin, &table))?; let old_quorum = Self::inner_update_table_quorum(&table, new_quorum); @@ -1333,6 +1335,30 @@ pub mod pallet { Ok(()) } + /// Checks whether the origin is either `Root` or the recorded owner of `table`. + /// + /// Returns: + /// - `Ok(stored_owner)` if the origin is `Root`, where `stored_owner` is the current + /// value in [`TableOwners`] (may be `None` if no owner is recorded), + /// - `Ok(Some(caller))` if the origin is a signed account that matches the stored owner, + /// - `Err(UnsignedTransaction)` if the origin is neither signed nor root, + /// - `Err(InsufficientPermissions)` if the signed account is not the stored owner. + fn ensure_root_or_owner( + origin: OriginFor, + table: &TableIdentifier, + ) -> Result, DispatchError> { + let maybe_owner = TableOwners::::get(table); + match origin.into() { + Ok(RawOrigin::Root) => Ok(maybe_owner), + Ok(RawOrigin::Signed(who)) => { + Ok(Some(maybe_owner.filter(|owner| owner == &who).ok_or( + pallet_permissions::Error::::InsufficientPermissions, + )?)) + } + _ => Err(pallet_permissions::Error::::UnsignedTransaction)?, + } + } + /// Returns the schema for the given table identifier. pub fn table_schema( table_identifier: TableIdentifier, diff --git a/pallets/tables/src/tests.rs b/pallets/tables/src/tests.rs index 450cfa26..8bc3bd2c 100644 --- a/pallets/tables/src/tests.rs +++ b/pallets/tables/src/tests.rs @@ -2081,3 +2081,78 @@ fn create_table_with_sci_metadata_fails_for_unpermissioned_user() { ); }); } + +/// Helper: create a community table owned by `user(1)` in the test namespace. +/// Returns the normalised `TableIdentifier`. +fn create_community_table_for_user1() -> TableIdentifier { + let (who, signer) = user(1); + let test_identifier = TableIdentifier::from_str_unchecked_with_preserved_casing( + "VOTES", + "FUNNAME_5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT", + ); + let ddl = "CREATE TABLE IF NOT EXISTS FUNNAME_5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT.VOTES (TIME_STAMP TIMESTAMP NOT NULL, BLOCK_NUMBER BIGINT NOT NULL, PRIMARY KEY (BLOCK_NUMBER));"; + let create_statement: CreateStatement = + BoundedVec::try_from(ddl.as_bytes().to_vec()).expect("fits"); + let tables: UpdateTableList = BoundedVec::try_from(vec![UpdateTable { + ident: test_identifier.clone(), + create_statement, + table_type: TableType::Community, + commitment: CommitmentCreationCmd::Empty(CommitmentSchemeFlags::default()), + source: Source::Ethereum, + }]) + .expect("fits"); + assert_ok!(pallet_balances::Pallet::::mint_into( + &who, + crate::CREATE_COST * 10, + )); + assert_ok!(Tables::create_tables(signer, tables)); + test_identifier.try_normalize().unwrap() +} + +#[test] +fn update_table_quorum_by_table_owner_succeeds() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + create_namespace_for_testing("FUNNAME_5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT"); + + let (who, signer) = user(1); + let ident = create_community_table_for_user1(); + + assert_eq!(TableOwners::::get(&ident), Some(who)); + assert!(!pallet_permissions::Pallet::::has_permissions( + &user(1).0, + &PermissionLevel::TablesPallet(TablesPalletPermission::EditSchema), + )); + + let new_quorum = InsertQuorumSize { + public: None, + privileged: Some(0), + }; + + assert_ok!(Tables::update_table_quorum( + signer, + ident.clone(), + new_quorum + )); + assert_eq!(TableInsertQuorums::::get(&ident), new_quorum); + }); +} + +#[test] +fn update_table_quorum_fails_for_non_owner_without_permission() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + create_namespace_for_testing("FUNNAME_5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT"); + + let ident = create_community_table_for_user1(); + let new_quorum = InsertQuorumSize { + public: None, + privileged: Some(0), + }; + let (_who2, signer2) = user(2); + assert_err!( + Tables::update_table_quorum(signer2, ident, new_quorum), + pallet_permissions::Error::::InsufficientPermissions, + ); + }); +}