diff --git a/near-plugins-derive/src/access_controllable.rs b/near-plugins-derive/src/access_controllable.rs index 15533fc..3569815 100644 --- a/near-plugins-derive/src/access_controllable.rs +++ b/near-plugins-derive/src/access_controllable.rs @@ -194,6 +194,16 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream Some(self.revoke_super_admin_unchecked(account_id)) } + fn transfer_super_admin(&mut self, account_id: &::near_sdk::AccountId) -> Option { + let current_super_admin = ::near_sdk::env::predecessor_account_id(); + if !self.is_super_admin(¤t_super_admin) { + return None; + } + + self.revoke_super_admin_unchecked(¤t_super_admin); + Some(self.add_super_admin_unchecked(&account_id)) + } + /// Revokes super-admin permissions from `account_id` without checking any /// permissions. It returns whether `account_id` was a super-admin. fn revoke_super_admin_unchecked(&mut self, account_id: &::near_sdk::AccountId) -> bool { @@ -565,6 +575,10 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream self.acl_get_or_init().revoke_super_admin(&account_id) } + fn acl_transfer_super_admin(&mut self, account_id: ::near_sdk::AccountId) -> Option { + self.acl_get_or_init().transfer_super_admin(&account_id) + } + fn acl_add_admin(&mut self, role: String, account_id: ::near_sdk::AccountId) -> Option { let role: #role_type = ::std::convert::TryFrom::try_from(role.as_str()).unwrap_or_else(|_| ::near_sdk::env::panic_str(#ERR_PARSE_ROLE)); self.acl_get_or_init().add_admin(role, &account_id) diff --git a/near-plugins-derive/tests/access_controllable.rs b/near-plugins-derive/tests/access_controllable.rs index bc975ce..87de2cf 100644 --- a/near-plugins-derive/tests/access_controllable.rs +++ b/near-plugins-derive/tests/access_controllable.rs @@ -397,6 +397,61 @@ async fn test_acl_revoke_super_admin() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_acl_transfer_super_admin() -> anyhow::Result<()> { + let setup = Setup::new().await?; + let super_admin = setup.new_super_admin_account().await?; + let new_super_admin = setup.worker.dev_create_account().await?; + + setup + .contract + .assert_acl_is_super_admin(true, setup.contract_account(), super_admin.id()) + .await; + + // Create caller account. + let caller_unauth = setup.worker.dev_create_account().await?; + + // Transfer is a no-op if caller is not a super-admin. + let res = setup + .contract + .acl_transfer_super_admin(&caller_unauth, super_admin.id()) + .await?; + assert_eq!(res, None); + setup + .contract + .assert_acl_is_super_admin(true, setup.contract_account(), super_admin.id()) + .await; + setup + .contract + .assert_acl_is_super_admin(false, setup.contract_account(), new_super_admin.id()) + .await; + + // Transfer succeeds if the caller is a super-admin. + let res = setup + .contract + .acl_transfer_super_admin(&super_admin, new_super_admin.id()) + .await?; + assert_eq!(res, Some(true)); + setup + .contract + .assert_acl_is_super_admin(false, setup.contract_account(), super_admin.id()) + .await; + setup + .contract + .assert_acl_is_super_admin(true, setup.contract_account(), new_super_admin.id()) + .await; + + // Transfer to an account that is already super-admin returns `Some(false)`. + let admin = setup.new_super_admin_account().await?; + let res = setup + .contract + .acl_transfer_super_admin(&new_super_admin, admin.id()) + .await?; + assert_eq!(res, Some(false)); + + Ok(()) +} + #[tokio::test] async fn test_acl_revoke_super_admin_unchecked() -> anyhow::Result<()> { let setup = Setup::new().await?; diff --git a/near-plugins-derive/tests/common/access_controllable_contract.rs b/near-plugins-derive/tests/common/access_controllable_contract.rs index a4379e6..670f4f7 100644 --- a/near-plugins-derive/tests/common/access_controllable_contract.rs +++ b/near-plugins-derive/tests/common/access_controllable_contract.rs @@ -100,6 +100,24 @@ impl AccessControllableContract { Ok(res) } + pub async fn acl_transfer_super_admin( + &self, + caller: &Account, + account_id: &AccountId, + ) -> anyhow::Result> { + let res = caller + .call(self.contract.id(), "acl_transfer_super_admin") + .args_json(json!({ + "account_id": account_id, + })) + .max_gas() + .transact() + .await? + .into_result()? + .json::>()?; + Ok(res) + } + pub async fn acl_revoke_super_admin_unchecked( &self, caller: &Account, diff --git a/near-plugins/src/access_controllable.rs b/near-plugins/src/access_controllable.rs index 927ba1e..d03918e 100644 --- a/near-plugins/src/access_controllable.rs +++ b/near-plugins/src/access_controllable.rs @@ -108,6 +108,45 @@ pub trait AccessControllable { /// ``` fn acl_revoke_super_admin(&mut self, account_id: AccountId) -> Option; + /// Transfer super-admin permissions from the predecessor to `account_id` provided that the + /// predecessor has sufficient permissions, i.e. is a super-admin as defined + /// by [`acl_is_super_admin`]. This function allows a super-admin to revoke the permission from + /// themselves and add `account_id` as super-admin. While it is a helper for use cases which + /// require this transfer, it should be noted that `AccessControllable` allows having more than + /// one super-admin. + /// + /// In case of sufficient permissions, the returned `Some(bool)` indicates + /// whether `account_id` is a new super-admin. Without permissions, `None` is + /// returned and internal state is not modified. + /// + /// If super-admin permissions are transferred, the following events will be + /// emitted: + /// + /// ```json + /// { + /// "standard":"AccessControllable", + /// "version":"1.0.0", + /// "event":"super_admin_revoked", + /// "data":{ + /// "account":"", + /// "by":"" + /// } + /// } + /// ``` + /// + /// ```json + /// { + /// "standard":"AccessControllable", + /// "version":"1.0.0", + /// "event":"super_admin_added", + /// "data":{ + /// "account":"", + /// "by":"" + /// } + /// } + /// ``` + fn acl_transfer_super_admin(&mut self, account_id: AccountId) -> Option; + /// Makes `account_id` an admin provided that the predecessor has sufficient /// permissions, i.e. is an admin as defined by [`acl_is_admin`]. ///