diff --git a/near-plugins-derive/src/access_controllable.rs b/near-plugins-derive/src/access_controllable.rs index 2cedb1a..15533fc 100644 --- a/near-plugins-derive/src/access_controllable.rs +++ b/near-plugins-derive/src/access_controllable.rs @@ -187,6 +187,13 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.contains(super_admin) } + fn revoke_super_admin(&mut self, account_id: &::near_sdk::AccountId) -> Option { + if !self.is_super_admin(&::near_sdk::env::predecessor_account_id()) { + return None; + } + Some(self.revoke_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 { @@ -554,6 +561,10 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream return_if_none!(self.acl_get_storage(), false).is_super_admin(&account_id) } + fn acl_revoke_super_admin(&mut self, account_id: ::near_sdk::AccountId) -> Option { + self.acl_get_or_init().revoke_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 3de5d85..bc975ce 100644 --- a/near-plugins-derive/tests/access_controllable.rs +++ b/near-plugins-derive/tests/access_controllable.rs @@ -350,6 +350,53 @@ async fn test_acl_add_super_admin_unchecked() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_acl_revoke_super_admin() -> anyhow::Result<()> { + let setup = Setup::new().await?; + let super_admin = setup.new_super_admin_account().await?; + + setup + .contract + .assert_acl_is_super_admin(true, setup.contract_account(), super_admin.id()) + .await; + + // Create revoker accounts. + let revoker_unauth = setup.worker.dev_create_account().await?; + let revoker_auth = setup.new_super_admin_account().await?; + + // Revoke is a no-op if revoker is not a super-admin. + let res = setup + .contract + .acl_revoke_super_admin(&revoker_unauth, super_admin.id()) + .await?; + assert_eq!(res, None); + setup + .contract + .assert_acl_is_super_admin(true, setup.contract_account(), super_admin.id()) + .await; + + // Revoke succeeds if the revoker is a super-admin. + let res = setup + .contract + .acl_revoke_super_admin(&revoker_auth, super_admin.id()) + .await?; + assert_eq!(res, Some(true)); + setup + .contract + .assert_acl_is_super_admin(false, setup.contract_account(), super_admin.id()) + .await; + + // Revoking from an account which isn't super-admin returns `Some(false)`. + let account = setup.worker.dev_create_account().await?; + let res = setup + .contract + .acl_revoke_super_admin(&revoker_auth, account.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 f544440..a4379e6 100644 --- a/near-plugins-derive/tests/common/access_controllable_contract.rs +++ b/near-plugins-derive/tests/common/access_controllable_contract.rs @@ -82,6 +82,24 @@ impl AccessControllableContract { .await } + pub async fn acl_revoke_super_admin( + &self, + caller: &Account, + account_id: &AccountId, + ) -> anyhow::Result> { + let res = caller + .call(self.contract.id(), "acl_revoke_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 2a24433..927ba1e 100644 --- a/near-plugins/src/access_controllable.rs +++ b/near-plugins/src/access_controllable.rs @@ -83,6 +83,31 @@ pub trait AccessControllable { /// grantee of any role. fn acl_is_super_admin(&self, account_id: AccountId) -> bool; + /// Revoke super-admin permissions from `account_id` provided that the + /// predecessor has sufficient permissions, i.e. is a super-admin as defined + /// by [`acl_is_super_admin`]. This means a super-admin may revoke + /// super-admin permissions from any other super-admin. + /// + /// In case of sufficient permissions, the returned `Some(bool)` indicates + /// whether `account_id` was a super-admin. Without permissions, `None` is + /// returned and internal state is not modified. + /// + /// If super-admin permissions are revoked, the following event will be + /// emitted: + /// + /// ```json + /// { + /// "standard":"AccessControllable", + /// "version":"1.0.0", + /// "event":"super_admin_revoked", + /// "data":{ + /// "account":"", + /// "by":"" + /// } + /// } + /// ``` + fn acl_revoke_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`]. ///