From ba685ccbae5a9a3f4c84c93bdc5ab1a33d99e1c2 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 26 Dec 2024 13:02:24 +0100 Subject: [PATCH] Implement access control for AccountStore members (#3204) --- parachain/pallets/omni-account/src/lib.rs | 143 ++++++++- parachain/pallets/omni-account/src/mock.rs | 95 +++++- parachain/pallets/omni-account/src/tests.rs | 279 ++++++++++++++++-- parachain/runtime/litentry/src/lib.rs | 86 ++++++ parachain/runtime/paseo/src/lib.rs | 86 ++++++ .../core/native-task/receiver/src/lib.rs | 7 +- 6 files changed, 669 insertions(+), 27 deletions(-) diff --git a/parachain/pallets/omni-account/src/lib.rs b/parachain/pallets/omni-account/src/lib.rs index a499e8bbe7..88165c979d 100644 --- a/parachain/pallets/omni-account/src/lib.rs +++ b/parachain/pallets/omni-account/src/lib.rs @@ -30,13 +30,13 @@ pub use pallet::*; use frame_support::pallet_prelude::*; use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo}, - traits::{IsSubType, UnfilteredDispatchable}, + traits::{InstanceFilter, IsSubType, UnfilteredDispatchable}, }; use frame_system::pallet_prelude::*; use sp_core::H256; use sp_runtime::traits::Dispatchable; use sp_std::boxed::Box; -use sp_std::vec::Vec; +use sp_std::{vec, vec::Vec}; pub type MemberCount = u32; @@ -68,7 +68,6 @@ pub mod pallet { #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] - #[pallet::without_storage_info] pub struct Pallet(_); #[pallet::config] @@ -106,6 +105,36 @@ pub mod pallet { /// Convert an `Identity` to OmniAccount type type OmniAccountConverter: OmniAccountConverter; + + /// The permissions that a member account can have + /// The instance filter determines whether a given call may can be dispatched under this type. + /// + /// IMPORTANT: `Default` must be provided and MUST BE the the *most permissive* value. + type Permission: Parameter + + Member + + Ord + + PartialOrd + + Default + + InstanceFilter<::RuntimeCall> + + MaxEncodedLen; + + /// The maximum number of permissions that a member account can have + #[pallet::constant] + type MaxPermissions: Get; + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn integrity_test() { + assert!( + ::MaxAccountStoreLength::get() > 0, + "MaxAccountStoreLength must be greater than 0" + ); + assert!( + ::MaxPermissions::get() > 0, + "MaxPermissions must be greater than 0" + ); + } } pub type MemberAccounts = BoundedVec::MaxAccountStoreLength>; @@ -115,6 +144,7 @@ pub mod pallet { /// A map between OmniAccount and its MemberAccounts (a bounded vector of MemberAccount) #[pallet::storage] + #[pallet::unbounded] #[pallet::getter(fn account_store)] pub type AccountStore = StorageMap>; @@ -124,6 +154,21 @@ pub mod pallet { pub type MemberAccountHash = StorageMap; + #[pallet::type_value] + pub fn DefaultPermissions() -> BoundedVec { + BoundedVec::try_from(vec![T::Permission::default()]).expect("default permission") + } + + /// A map between hash of MemberAccount and its permissions + #[pallet::storage] + pub type MemberAccountPermissions = StorageMap< + Hasher = Blake2_128Concat, + Key = H256, + Value = BoundedVec, + QueryKind = ValueQuery, + OnEmpty = DefaultPermissions, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -153,6 +198,8 @@ pub mod pallet { IntentRequested { who: T::AccountId, intent: Intent }, /// Intent is executed IntentExecuted { who: T::AccountId, intent: Intent, result: IntentExecutionResult }, + /// Member permission set + AccountPermissionsSet { who: T::AccountId, member_account_hash: H256 }, /// An auth token is requested AuthTokenRequested { who: T::AccountId, expires_at: BlockNumberFor }, } @@ -165,6 +212,8 @@ pub mod pallet { InvalidAccount, UnknownAccountStore, EmptyAccount, + NoPermission, + PermissionsLenLimitReached, } #[pallet::call] @@ -181,6 +230,7 @@ pub mod pallet { let _ = T::TEECallOrigin::ensure_origin(origin)?; let omni_account = MemberAccountHash::::get(member_account_hash) .ok_or(Error::::AccountNotFound)?; + Self::ensure_permission(call.as_ref(), member_account_hash)?; let result = call.dispatch(RawOrigin::OmniAccount(omni_account.clone()).into()); system::Pallet::::inc_account_nonce(&omni_account); Self::deposit_event(Event::DispatchedAsOmniAccount { @@ -204,6 +254,7 @@ pub mod pallet { let _ = T::TEECallOrigin::ensure_origin(origin)?; let omni_account = MemberAccountHash::::get(member_account_hash) .ok_or(Error::::AccountNotFound)?; + Self::ensure_permission(call.as_ref(), member_account_hash)?; let result: Result< PostDispatchInfo, sp_runtime::DispatchErrorWithPostInfo, @@ -233,7 +284,8 @@ pub mod pallet { #[pallet::weight((195_000_000, DispatchClass::Normal))] pub fn add_account( origin: OriginFor, - member_account: MemberAccount, // account to be added + member_account: MemberAccount, // account to be added + permissions: Option>, // permissions for the account ) -> DispatchResult { // mutation of AccountStore requires `OmniAccountOrigin`, same as "remove" and "publicize" let who = T::OmniAccountOrigin::ensure_origin(origin)?; @@ -249,8 +301,16 @@ pub mod pallet { member_accounts .try_push(member_account) .map_err(|_| Error::::AccountStoreLenLimitReached)?; + let member_permissions: BoundedVec = permissions + .map_or_else( + || vec![T::Permission::default()], + |p| if p.is_empty() { vec![T::Permission::default()] } else { p }, + ) + .try_into() + .map_err(|_| Error::::PermissionsLenLimitReached)?; MemberAccountHash::::insert(hash, who.clone()); + MemberAccountPermissions::::insert(hash, member_permissions); AccountStore::::insert(who.clone(), member_accounts.clone()); Self::deposit_event(Event::AccountAdded { @@ -277,6 +337,7 @@ pub mod pallet { member_accounts.retain(|member| { if member_account_hashes.contains(&member.hash()) { MemberAccountHash::::remove(member.hash()); + MemberAccountPermissions::::remove(member.hash()); false } else { true @@ -352,8 +413,13 @@ pub mod pallet { .try_push(member_account.clone()) .map_err(|_| Error::::AccountStoreLenLimitReached)?; } + let mut permissions = BoundedVec::::new(); + permissions + .try_push(T::Permission::default()) + .map_err(|_| Error::::PermissionsLenLimitReached)?; MemberAccountHash::::insert(member_account.hash(), who_account.clone()); + MemberAccountPermissions::::insert(member_account.hash(), permissions); AccountStore::::insert(who_account.clone(), member_accounts.clone()); Self::deposit_event(Event::AccountStoreUpdated { who: who_account, @@ -378,6 +444,21 @@ pub mod pallet { #[pallet::call_index(9)] #[pallet::weight((195_000_000, DispatchClass::Normal))] + pub fn set_permissions( + origin: OriginFor, + member_account_hash: H256, + permissions: Vec, + ) -> DispatchResult { + let who = T::OmniAccountOrigin::ensure_origin(origin)?; + let member_permissions: BoundedVec = + { permissions.try_into().map_err(|_| Error::::PermissionsLenLimitReached)? }; + MemberAccountPermissions::::insert(member_account_hash, member_permissions); + Self::deposit_event(Event::AccountPermissionsSet { who, member_account_hash }); + Ok(()) + } + + #[pallet::call_index(10)] + #[pallet::weight((195_000_000, DispatchClass::Normal))] pub fn auth_token_requested( origin: OriginFor, who: T::AccountId, @@ -412,8 +493,13 @@ pub mod pallet { member_accounts .try_push(identity.into()) .map_err(|_| Error::::AccountStoreLenLimitReached)?; + let mut permissions = BoundedVec::::new(); + permissions + .try_push(T::Permission::default()) + .map_err(|_| Error::::PermissionsLenLimitReached)?; MemberAccountHash::::insert(hash, omni_account.clone()); + MemberAccountPermissions::::insert(hash, permissions); AccountStore::::insert(omni_account.clone(), member_accounts.clone()); Self::deposit_event(Event::AccountStoreCreated { who: omni_account.clone() }); @@ -424,6 +510,55 @@ pub mod pallet { Ok(member_accounts) } + + fn ensure_permission( + call: &::RuntimeCall, + member_account_hash: H256, + ) -> Result<(), Error> { + let member_permissions = MemberAccountPermissions::::get(member_account_hash); + + ensure!( + member_permissions.iter().any(|permission| permission.filter(call)), + Error::::NoPermission + ); + + match call.is_sub_type() { + Some(Call::add_account { permissions: ref new_account_permissions, .. }) => { + // If member has default permission, they can add accounts with any permission + if member_permissions.contains(&T::Permission::default()) { + return Ok(()); + } + match new_account_permissions { + Some(new_permissions) => { + // an account can only add another account with the same or less permissions + if new_permissions.is_empty() + || !new_permissions.iter().all(|p| member_permissions.contains(p)) + { + return Err(Error::::NoPermission); + } + }, + None => { + // None is equivalent to default permission. It should not be allowed + // if the member_permissions have no default permission + return Err(Error::::NoPermission); + }, + } + }, + Some(Call::set_permissions { permissions: ref new_permissions, .. }) => { + // If member has default permission, they can set permissions to any value + if member_permissions.contains(&T::Permission::default()) { + return Ok(()); + } + // an account can only set permissions to the same or less permissions + if !new_permissions.iter().all(|p| member_permissions.contains(p)) { + return Err(Error::::NoPermission); + } + }, + _ => return Ok(()), + } + + Ok(()) + } } } diff --git a/parachain/pallets/omni-account/src/mock.rs b/parachain/pallets/omni-account/src/mock.rs index b7ab3f457c..1c51bff75f 100644 --- a/parachain/pallets/omni-account/src/mock.rs +++ b/parachain/pallets/omni-account/src/mock.rs @@ -14,17 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use crate::{self as pallet_omni_account, Encode, EnsureOmniAccount}; +use crate::{self as pallet_omni_account, Decode, Encode, EnsureOmniAccount, MaxEncodedLen}; use core_primitives::{DefaultOmniAccountConverter, Identity, MemberAccount}; use frame_support::{ assert_ok, derive_impl, pallet_prelude::EnsureOrigin, parameter_types, - traits::{ConstU32, ConstU64}, + traits::{ConstU32, ConstU64, InstanceFilter}, }; use frame_system::EnsureRoot; pub use pallet_teebag::test_util::get_signer; use pallet_teebag::test_util::{TEST8_CERT, TEST8_SIGNER_PUB, TEST8_TIMESTAMP, URL}; +use sp_core::RuntimeDebug; use sp_keyring::AccountKeyring; use sp_runtime::{ traits::{IdentifyAccount, IdentityLookup, Verify}, @@ -96,6 +97,10 @@ pub fn charlie() -> Accounts { create_accounts(AccountKeyring::Charlie) } +pub fn dave() -> Accounts { + create_accounts(AccountKeyring::Dave) +} + pub fn public_member_account(accounts: Accounts) -> MemberAccount { MemberAccount::Public(accounts.identity) } @@ -158,6 +163,90 @@ impl pallet_teebag::Config for Test { type WeightInfo = (); } +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Ord, + PartialOrd, + Encode, + Decode, + RuntimeDebug, + MaxEncodedLen, + scale_info::TypeInfo, +)] +pub enum OmniAccountPermission { + All, + AccountManagement, + RequestNativeIntent, + RequestEthereumIntent, + RequestSolanaIntent, +} + +impl Default for OmniAccountPermission { + fn default() -> Self { + Self::All + } +} + +impl InstanceFilter for OmniAccountPermission { + fn filter(&self, call: &RuntimeCall) -> bool { + match self { + Self::All => true, + Self::AccountManagement => { + matches!( + call, + RuntimeCall::OmniAccount(pallet_omni_account::Call::add_account { .. }) + | RuntimeCall::OmniAccount( + pallet_omni_account::Call::remove_accounts { .. } + ) | RuntimeCall::OmniAccount( + pallet_omni_account::Call::publicize_account { .. } + ) | RuntimeCall::OmniAccount(pallet_omni_account::Call::set_permissions { .. }) + ) + }, + Self::RequestNativeIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!( + intent, + pallet_omni_account::Intent::SystemRemark(_) + | pallet_omni_account::Intent::TransferNative(_) + ) + } else { + false + } + }, + Self::RequestEthereumIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!( + intent, + pallet_omni_account::Intent::TransferEthereum(_) + | pallet_omni_account::Intent::CallEthereum(_) + ) + } else { + false + } + }, + Self::RequestSolanaIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!(intent, pallet_omni_account::Intent::TransferSolana(_)) + } else { + false + } + }, + } + } +} + impl pallet_omni_account::Config for Test { type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; @@ -166,6 +255,8 @@ impl pallet_omni_account::Config for Test { type MaxAccountStoreLength = ConstU32<3>; type OmniAccountOrigin = EnsureOmniAccount; type OmniAccountConverter = DefaultOmniAccountConverter; + type MaxPermissions = ConstU32<4>; + type Permission = OmniAccountPermission; } pub fn get_tee_signer() -> SystemAccountId { diff --git a/parachain/pallets/omni-account/src/tests.rs b/parachain/pallets/omni-account/src/tests.rs index 2e0d3057df..0689fe5135 100644 --- a/parachain/pallets/omni-account/src/tests.rs +++ b/parachain/pallets/omni-account/src/tests.rs @@ -22,8 +22,12 @@ use sp_core::H160; use sp_runtime::{traits::BadOrigin, ModuleError}; use sp_std::vec; -fn add_account_call(account: MemberAccount) -> Box { - let call = RuntimeCall::OmniAccount(crate::Call::add_account { member_account: account }); +fn add_account_call>( + account: MemberAccount, + permissions: Option::Permission>>, +) -> Box { + let call = + RuntimeCall::OmniAccount(crate::Call::add_account { member_account: account, permissions }); Box::new(call) } @@ -47,6 +51,15 @@ fn make_balance_transfer_call(dest: AccountId, value: Balance) -> Box, +) -> Box { + let call = + RuntimeCall::OmniAccount(crate::Call::set_permissions { member_account_hash, permissions }); + Box::new(call) +} + #[test] fn create_account_store_works() { new_test_ext().execute_with(|| { @@ -81,7 +94,7 @@ fn create_account_store_works() { fn add_account_without_creating_store_fails() { new_test_ext().execute_with(|| { let tee_signer = get_tee_signer(); - let call = add_account_call(private_member_account(bob())); + let call = add_account_call::(private_member_account(bob()), None); assert_noop!( OmniAccount::dispatch_as_omni_account( @@ -111,7 +124,7 @@ fn add_account_works() { alice().identity, )); - let call = add_account_call(bob.clone()); + let call = add_account_call::(bob.clone(), Some(vec![])); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -139,7 +152,7 @@ fn add_account_works() { expected_member_accounts ); - let call = add_account_call(charlie.clone()); + let call = add_account_call::(charlie.clone(), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -165,7 +178,17 @@ fn add_account_works() { ); assert!(MemberAccountHash::::contains_key(bob.hash())); + assert!(MemberAccountPermissions::::contains_key(bob.hash())); assert!(MemberAccountHash::::contains_key(charlie.hash())); + assert!(MemberAccountPermissions::::contains_key(charlie.hash())); + assert_eq!( + MemberAccountPermissions::::get(bob.hash()).to_vec(), + vec![OmniAccountPermission::All] + ); + assert_eq!( + MemberAccountPermissions::::get(charlie.hash()).to_vec(), + vec![OmniAccountPermission::All] + ); }); } @@ -176,12 +199,12 @@ fn add_account_origin_check_works() { let bob = private_member_account(bob()); assert_noop!( - OmniAccount::add_account(RuntimeOrigin::signed(tee_signer), bob.clone()), + OmniAccount::add_account(RuntimeOrigin::signed(tee_signer), bob.clone(), None), BadOrigin ); assert_noop!( - OmniAccount::add_account(RuntimeOrigin::signed(alice().omni_account), bob), + OmniAccount::add_account(RuntimeOrigin::signed(alice().omni_account), bob, None), BadOrigin ); }); @@ -198,7 +221,7 @@ fn add_account_with_already_linked_account_fails() { alice().identity.clone(), )); - let call = add_account_call(bob.clone()); + let call = add_account_call::(bob.clone(), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -232,7 +255,7 @@ fn add_account_with_already_linked_account_fails() { charlie().identity, )); - let call = add_account_call(public_member_account(alice())); + let call = add_account_call::(public_member_account(alice()), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), charlie().identity.hash(), @@ -275,10 +298,10 @@ fn add_account_store_len_limit_reached_works() { AccountStore::::insert(alice().omni_account, member_accounts); - let call = add_account_call(MemberAccount::Private( - vec![7, 8, 9], - H256::from(blake2_256(&[7, 8, 9])), - )); + let call = add_account_call::( + MemberAccount::Private(vec![7, 8, 9], H256::from(blake2_256(&[7, 8, 9]))), + None, + ); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), @@ -313,7 +336,7 @@ fn remove_account_works() { alice().identity, )); - let call = add_account_call(bob.clone()); + let call = add_account_call::(bob.clone(), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -379,6 +402,7 @@ fn remove_account_works() { expected_member_accounts ); assert!(!MemberAccountHash::::contains_key(bob.hash())); + assert!(!MemberAccountPermissions::::contains_key(bob.hash())); let call = remove_accounts_call(vec![alice().identity.hash()]); assert_ok!(OmniAccount::dispatch_as_omni_account( @@ -403,7 +427,7 @@ fn remove_account_empty_account_check_works() { )); let bob = private_member_account(bob()); - let call = add_account_call(bob); + let call = add_account_call::(bob, None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -446,7 +470,7 @@ fn publicize_account_works() { alice().identity.clone(), )); - let call = add_account_call(private_bob.clone()); + let call = add_account_call::(private_bob.clone(), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -525,7 +549,7 @@ fn publicize_account_identity_not_found_works() { alice().identity, )); - let call = add_account_call(bob.clone()); + let call = add_account_call::(bob.clone(), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -566,7 +590,7 @@ fn request_intent_works() { alice().identity )); - let call = add_account_call(bob); + let call = add_account_call::(bob, None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -616,7 +640,7 @@ fn dispatch_as_signed_works() { alice().identity, )); - let call = add_account_call(private_member_account(bob())); + let call = add_account_call::(private_member_account(bob()), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -658,7 +682,7 @@ fn dispatch_as_omni_account_increments_omni_account_nonce() { assert_eq!(System::account_nonce(alice().omni_account), 0); - let call = add_account_call(bob.clone()); + let call = add_account_call::(bob.clone(), None); assert_ok!(OmniAccount::dispatch_as_omni_account( RuntimeOrigin::signed(tee_signer.clone()), alice().identity.hash(), @@ -699,6 +723,221 @@ fn dispatch_as_signed_account_increments_omni_account_nonce() { }); } +#[test] +fn ensure_permission_works() { + new_test_ext().execute_with(|| { + // Create account store + let tee_signer = get_tee_signer(); + + assert_ok!(OmniAccount::create_account_store( + RuntimeOrigin::signed(tee_signer.clone()), + alice().identity, + )); + + // Add account member without permissions to remove accounts + let bob = private_member_account(bob()); + let bob_permissions = vec![ + OmniAccountPermission::RequestEthereumIntent, + OmniAccountPermission::RequestSolanaIntent, + OmniAccountPermission::AccountManagement, + ]; + + let call = add_account_call::(bob.clone(), Some(bob_permissions.clone())); + assert_ok!(OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + alice().identity.hash(), + call, + OmniAccountAuthType::Web3 + )); + + // An Account cannot be added with more permissions than the account that added it + let charlie = private_member_account(charlie()); + let call = add_account_call::(charlie.clone(), None); // default permission + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + let charlie_permissions = vec![OmniAccountPermission::All]; + let call = add_account_call::(charlie.clone(), Some(charlie_permissions)); + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + let mut charlie_permissions = vec![OmniAccountPermission::RequestNativeIntent]; + charlie_permissions.extend_from_slice(&bob_permissions); + let call = add_account_call::(charlie.clone(), Some(charlie_permissions)); + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + let charlie_permissions = vec![ + OmniAccountPermission::RequestEthereumIntent, + OmniAccountPermission::RequestSolanaIntent, + ]; + let call = add_account_call::(charlie.clone(), Some(charlie_permissions)); + assert_ok!(OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + )); + + // An account with no permissions cannot remove accounts + let call = remove_accounts_call(vec![alice().identity.hash()]); + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + charlie.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + // Permissions should also work for dispatch_as_signed + assert_ok!(Balances::transfer_keep_alive( + RuntimeOrigin::signed(alice().native_account), + alice().omni_account, + 6 + )); + let call = make_balance_transfer_call(dave().native_account, 5); + assert_noop!( + OmniAccount::dispatch_as_signed( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call.clone(), + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + assert_ok!(OmniAccount::dispatch_as_signed( + RuntimeOrigin::signed(tee_signer), + alice().identity.hash(), + call, + OmniAccountAuthType::Web3 + )); + }); +} + +#[test] +fn set_permissions_works() { + new_test_ext().execute_with(|| { + // Create account store and add accounts + let tee_signer = get_tee_signer(); + + assert_ok!(OmniAccount::create_account_store( + RuntimeOrigin::signed(tee_signer.clone()), + alice().identity, + )); + + let bob = private_member_account(bob()); + let bob_permissions = vec![ + OmniAccountPermission::RequestEthereumIntent, + OmniAccountPermission::RequestSolanaIntent, + OmniAccountPermission::AccountManagement, + ]; + + let call = add_account_call::(bob.clone(), Some(bob_permissions.clone())); + assert_ok!(OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + alice().identity.hash(), + call, + OmniAccountAuthType::Web3 + )); + + let charlie = private_member_account(charlie()); + let charlie_permissions = vec![OmniAccountPermission::RequestNativeIntent]; + let call = add_account_call::(charlie.clone(), Some(charlie_permissions.clone())); + assert_ok!(OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + alice().identity.hash(), + call, + OmniAccountAuthType::Web3 + )); + + // Assert Set permissions + + // The caller cannot upgrade his permissions + let new_permissions = vec![OmniAccountPermission::All]; + let call = set_permissions_call(bob.hash(), new_permissions.clone()); + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + // The caller cannot set a permission that he does not have + let new_permissions = vec![OmniAccountPermission::RequestNativeIntent]; + let call = set_permissions_call(alice().identity.hash(), new_permissions.clone()); + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + // The caller can set permissions he has + let new_permissions = vec![ + OmniAccountPermission::AccountManagement, + OmniAccountPermission::RequestSolanaIntent, + ]; + let call = set_permissions_call(charlie.hash(), new_permissions.clone()); + assert_ok!(OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + bob.hash(), + call, + OmniAccountAuthType::Web3 + )); + + // The caller most have permission to set_permissions + let call = set_permissions_call(bob.hash(), charlie_permissions); + assert_noop!( + OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + charlie.hash(), + call, + OmniAccountAuthType::Web3 + ), + Error::::NoPermission + ); + + // The caller can set any permissions as if she has default permissions (All) + let new_permissions = vec![OmniAccountPermission::All]; + let call = set_permissions_call(charlie.hash(), new_permissions.clone()); + assert_ok!(OmniAccount::dispatch_as_omni_account( + RuntimeOrigin::signed(tee_signer.clone()), + alice().identity.hash(), + call, + OmniAccountAuthType::Web3 + )); + }); +} + #[test] fn auth_token_requested_works() { new_test_ext().execute_with(|| { diff --git a/parachain/runtime/litentry/src/lib.rs b/parachain/runtime/litentry/src/lib.rs index ff555e4d85..80e06926d1 100644 --- a/parachain/runtime/litentry/src/lib.rs +++ b/parachain/runtime/litentry/src/lib.rs @@ -1112,6 +1112,90 @@ impl pallet_identity_management::Config for Runtime { type MaxOIDCClientRedirectUris = ConstU32<10>; } +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Ord, + PartialOrd, + Encode, + Decode, + MaxEncodedLen, + RuntimeDebug, + scale_info::TypeInfo, +)] +pub enum OmniAccountPermission { + All, + AccountManagement, + RequestNativeIntent, + RequestEthereumIntent, + RequestSolanaIntent, +} + +impl Default for OmniAccountPermission { + fn default() -> Self { + Self::All + } +} + +impl InstanceFilter for OmniAccountPermission { + fn filter(&self, call: &RuntimeCall) -> bool { + match self { + Self::All => true, + Self::AccountManagement => { + matches!( + call, + RuntimeCall::OmniAccount(pallet_omni_account::Call::add_account { .. }) + | RuntimeCall::OmniAccount( + pallet_omni_account::Call::remove_accounts { .. } + ) | RuntimeCall::OmniAccount( + pallet_omni_account::Call::publicize_account { .. } + ) | RuntimeCall::OmniAccount(pallet_omni_account::Call::set_permissions { .. }) + ) + }, + Self::RequestNativeIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!( + intent, + pallet_omni_account::Intent::SystemRemark(_) + | pallet_omni_account::Intent::TransferNative(_) + ) + } else { + false + } + }, + Self::RequestEthereumIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!( + intent, + pallet_omni_account::Intent::TransferEthereum(_) + | pallet_omni_account::Intent::CallEthereum(_) + ) + } else { + false + } + }, + Self::RequestSolanaIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!(intent, pallet_omni_account::Intent::TransferSolana(_)) + } else { + false + } + }, + } + } +} + impl pallet_omni_account::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; @@ -1120,6 +1204,8 @@ impl pallet_omni_account::Config for Runtime { type MaxAccountStoreLength = ConstU32<64>; type OmniAccountOrigin = EnsureOmniAccount; type OmniAccountConverter = DefaultOmniAccountConverter; + type MaxPermissions = ConstU32<4>; + type Permission = OmniAccountPermission; } impl pallet_evm_assertions::Config for Runtime { diff --git a/parachain/runtime/paseo/src/lib.rs b/parachain/runtime/paseo/src/lib.rs index 597509e9b2..38402cc395 100644 --- a/parachain/runtime/paseo/src/lib.rs +++ b/parachain/runtime/paseo/src/lib.rs @@ -1157,6 +1157,90 @@ impl pallet_identity_management::Config for Runtime { type MaxOIDCClientRedirectUris = ConstU32<10>; } +#[derive( + Copy, + Clone, + PartialEq, + Eq, + Ord, + PartialOrd, + Encode, + Decode, + MaxEncodedLen, + RuntimeDebug, + scale_info::TypeInfo, +)] +pub enum OmniAccountPermission { + All, + AccountManagement, + RequestNativeIntent, + RequestEthereumIntent, + RequestSolanaIntent, +} + +impl Default for OmniAccountPermission { + fn default() -> Self { + Self::All + } +} + +impl InstanceFilter for OmniAccountPermission { + fn filter(&self, call: &RuntimeCall) -> bool { + match self { + Self::All => true, + Self::AccountManagement => { + matches!( + call, + RuntimeCall::OmniAccount(pallet_omni_account::Call::add_account { .. }) + | RuntimeCall::OmniAccount( + pallet_omni_account::Call::remove_accounts { .. } + ) | RuntimeCall::OmniAccount( + pallet_omni_account::Call::publicize_account { .. } + ) | RuntimeCall::OmniAccount(pallet_omni_account::Call::set_permissions { .. }) + ) + }, + Self::RequestNativeIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!( + intent, + pallet_omni_account::Intent::SystemRemark(_) + | pallet_omni_account::Intent::TransferNative(_) + ) + } else { + false + } + }, + Self::RequestEthereumIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!( + intent, + pallet_omni_account::Intent::TransferEthereum(_) + | pallet_omni_account::Intent::CallEthereum(_) + ) + } else { + false + } + }, + Self::RequestSolanaIntent => { + if let RuntimeCall::OmniAccount(pallet_omni_account::Call::request_intent { + intent, + }) = call + { + matches!(intent, pallet_omni_account::Intent::TransferSolana(_)) + } else { + false + } + }, + } + } +} + impl pallet_omni_account::Config for Runtime { type RuntimeOrigin = RuntimeOrigin; type RuntimeCall = RuntimeCall; @@ -1165,6 +1249,8 @@ impl pallet_omni_account::Config for Runtime { type MaxAccountStoreLength = ConstU32<64>; type OmniAccountOrigin = EnsureOmniAccount; type OmniAccountConverter = DefaultOmniAccountConverter; + type MaxPermissions = ConstU32<4>; + type Permission = OmniAccountPermission; } impl pallet_bitacross::Config for Runtime { diff --git a/tee-worker/identity/litentry/core/native-task/receiver/src/lib.rs b/tee-worker/identity/litentry/core/native-task/receiver/src/lib.rs index 98246dced5..cd0b63c59c 100644 --- a/tee-worker/identity/litentry/core/native-task/receiver/src/lib.rs +++ b/tee-worker/identity/litentry/core/native-task/receiver/src/lib.rs @@ -415,6 +415,10 @@ fn handle_trusted_call> = None; // default permissions + OpaqueCall::from_tuple(&compose_call!( &metadata, "OmniAccount", @@ -424,7 +428,8 @@ fn handle_trusted_call