diff --git a/Cargo.lock b/Cargo.lock index cc67c2f..eb5e970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,8 @@ dependencies = [ "ripemd", "serde", "sha2 0.10.8", + "strum", + "strum_macros", "tiny-keccak", ] @@ -379,6 +381,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -494,6 +502,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.17" @@ -639,6 +653,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.52", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 670bfd3..1c37d42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,6 @@ ripemd = "0.1.3" serde = { version = "1.0.145", default-features = false, features = ["derive"] } tiny-keccak = { version = "2.0.2", features = ["keccak"] } hex = "0.4.3" -base64 = "0.21.7" \ No newline at end of file +base64 = "0.21.7" +strum = "0.26" +strum_macros = "0.26.4" \ No newline at end of file diff --git a/src/access_control/contract.rs b/src/access_control/contract.rs new file mode 100644 index 0000000..77e29f4 --- /dev/null +++ b/src/access_control/contract.rs @@ -0,0 +1,98 @@ +use crate::access_control::AccessControl; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use strum::IntoEnumIterator; + +#[cw_serde] +pub struct QueryAllAddressesWithRoleResponse { + pub addresses: Vec, +} + +#[cw_serde] +pub struct QueryRolesResponse { + pub roles: Vec, +} + +pub fn query_all_addresses_with_role>( + deps: Deps, + role: T, +) -> StdResult { + let addresses_with_role: StdResult> = + AccessControl::get_all_addresses_with_role(deps.storage, &role).collect(); + + Ok(QueryAllAddressesWithRoleResponse { + addresses: addresses_with_role?, + }) +} + +pub fn query_roles>( + deps: Deps, + addr: Addr, +) -> StdResult> { + let roles = T::iter() // Automatically iterate over all roles + .filter(|role| AccessControl::has_role(deps.storage, role, &addr)) + .collect(); + + Ok(QueryRolesResponse { roles }) +} + +pub fn execute_grant_role_by_admin_role>( + deps: DepsMut, + env: Env, + info: MessageInfo, + role: T, + addr: Addr, + required_sender_role: T, +) -> StdResult { + AccessControl::ensure_has_role_or_superadmin( + &deps.as_ref(), + &env, + &required_sender_role, + &info.sender, + )?; + AccessControl::storage_set_role(deps.storage, &role, &addr)?; + + Ok(Response::new() + .add_attribute("action", "grant_role") + .add_attribute("sender", info.sender) + .add_attribute("role", role.as_ref()) + .add_attribute("addr", addr.to_string())) +} + +pub fn execute_revoke_role_by_admin_role>( + deps: DepsMut, + env: Env, + info: MessageInfo, + role: T, + addr: Addr, + required_sender_role: T, +) -> StdResult { + AccessControl::ensure_has_role_or_superadmin( + &deps.as_ref(), + &env, + &required_sender_role, + &info.sender, + )?; + AccessControl::storage_remove_role(deps.storage, &role, &addr)?; + + Ok(Response::new() + .add_attribute("action", "revoke_role") + .add_attribute("sender", info.sender) + .add_attribute("role", role.as_ref()) + .add_attribute("addr", addr.to_string())) +} + +pub fn execute_renounce_role>( + deps: DepsMut, + _env: Env, + info: MessageInfo, + role: T, +) -> StdResult { + AccessControl::ensure_has_role(&deps.as_ref(), &role, &info.sender)?; + AccessControl::storage_remove_role(deps.storage, &role, &info.sender)?; + + Ok(Response::new() + .add_attribute("action", "renounce_role") + .add_attribute("sender", info.sender) + .add_attribute("role", role.as_ref())) +} diff --git a/src/access_control/error.rs b/src/access_control/error.rs new file mode 100644 index 0000000..85d1f0e --- /dev/null +++ b/src/access_control/error.rs @@ -0,0 +1,20 @@ +use cosmwasm_std::{Addr, StdError}; + +pub fn no_role_error>(address: &Addr, role: &T) -> StdError { + StdError::generic_err(format!( + "Address {} does not have role {}", + address, + role.as_ref() + )) +} +pub fn insufficient_permissions_error() -> StdError { + StdError::generic_err("Insufficient permissions") +} + +pub fn role_already_exist_error>(role: &T) -> StdError { + StdError::generic_err(format!("Role {} already exist", role.as_ref())) +} + +pub fn sender_is_not_role_admin_error>(role: &T) -> StdError { + StdError::generic_err(format!("Sender is not admin of role {}", role.as_ref())) +} diff --git a/src/access_control/mod.rs b/src/access_control/mod.rs index bd5643d..4db5b97 100644 --- a/src/access_control/mod.rs +++ b/src/access_control/mod.rs @@ -1,3 +1,6 @@ +mod contract; +pub mod error; mod role; +pub use contract::*; pub use role::AccessControl; diff --git a/src/access_control/role.rs b/src/access_control/role.rs index a04a7bc..bc84ede 100644 --- a/src/access_control/role.rs +++ b/src/access_control/role.rs @@ -1,6 +1,10 @@ +use crate::access_control::error::{ + insufficient_permissions_error, no_role_error, role_already_exist_error, + sender_is_not_role_admin_error, +}; use crate::permissions::is_super_admin; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Deps, DepsMut, Env, StdError, StdResult, Storage}; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, Order, StdResult, Storage}; use cw_storage_plus::Map; use std::marker::PhantomData; @@ -45,45 +49,49 @@ impl> AccessControl { HAS_ROLE.has(storage, (role.as_ref(), address)) } - pub fn ensure_role_admin(deps: &Deps, env: &Env, sender: &Addr, role: &T) -> StdResult<()> { - if !is_super_admin(deps, env, sender)? { - let role_admin = Self::get_role_admin(deps.storage, role)?.ok_or( - StdError::generic_err(format!("No admin for role {}", role.as_ref())), - )?; - - if role_admin != sender { - return Err(StdError::generic_err(format!( - "Sender is not admin of role {}", - role.as_ref() - ))); + pub fn ensure_role_admin(deps: &Deps, sender: &Addr, role: &T) -> StdResult<()> { + if let Some(role_admin) = Self::get_role_admin(deps.storage, role)? { + if role_admin == sender { + return Ok(()); } } - Ok(()) + + Err(sender_is_not_role_admin_error(role)) } - pub fn give_role( + pub fn grant_role( deps: &mut DepsMut, - env: &Env, sender: &Addr, role: &T, address: &Addr, ) -> StdResult<()> { - Self::ensure_role_admin(&deps.as_ref(), env, sender, role)?; + Self::ensure_role_admin(&deps.as_ref(), sender, role)?; + Self::storage_set_role(deps.storage, role, address)?; + Ok(()) + } - HAS_ROLE.save(deps.storage, (role.as_ref(), address), &())?; + pub fn storage_set_role(storage: &mut dyn Storage, role: &T, address: &Addr) -> StdResult<()> { + HAS_ROLE.save(storage, (role.as_ref(), address), &())?; Ok(()) } - pub fn take_role( + pub fn revoke_role( deps: &mut DepsMut, - env: &Env, sender: &Addr, role: &T, address: &Addr, ) -> StdResult<()> { - Self::ensure_role_admin(&deps.as_ref(), env, sender, role)?; + Self::ensure_role_admin(&deps.as_ref(), sender, role)?; + Self::storage_remove_role(deps.storage, role, address)?; + Ok(()) + } - HAS_ROLE.remove(deps.storage, (role.as_ref(), address)); + pub fn storage_remove_role( + storage: &mut dyn Storage, + role: &T, + address: &Addr, + ) -> StdResult<()> { + HAS_ROLE.remove(storage, (role.as_ref(), address)); Ok(()) } @@ -93,10 +101,7 @@ impl> AccessControl { role_admin: Option<&Addr>, ) -> StdResult<()> { if Self::role_exists(storage, role) { - return Err(StdError::generic_err(format!( - "Role {} already exist", - role.as_ref() - ))); + return Err(role_already_exist_error(role)); } ROLE.save( @@ -112,12 +117,11 @@ impl> AccessControl { pub fn change_role_admin( deps: DepsMut, - env: &Env, sender: &Addr, role: &T, new_admin: &Addr, ) -> StdResult<()> { - Self::ensure_role_admin(&deps.as_ref(), env, sender, role)?; + Self::ensure_role_admin(&deps.as_ref(), sender, role)?; Self::_set_role_admin(deps.storage, role, new_admin) } @@ -125,27 +129,55 @@ impl> AccessControl { ROLE.has(storage, role.as_ref()) } - pub fn ensure_has_role_if_exists( - storage: &dyn Storage, + pub fn ensure_has_role_if_exists(deps: &Deps, role: &T, address: &Addr) -> StdResult<()> { + if Self::role_exists(deps.storage, role) { + Self::ensure_has_role(deps, role, address)?; + } + + Ok(()) + } + + pub fn ensure_has_role(deps: &Deps, role: &T, address: &Addr) -> StdResult<()> { + if Self::has_role(deps.storage, role, address) { + Ok(()) + } else { + Err(no_role_error(address, role)) + } + } + + pub fn ensure_has_role_or_superadmin( + deps: &Deps, + env: &Env, role: &T, address: &Addr, ) -> StdResult<()> { - if Self::role_exists(storage, role) { - Self::ensure_has_role(storage, role, address)?; + if Self::has_role(deps.storage, role, address) || is_super_admin(deps, env, address)? { + Ok(()) + } else { + Err(no_role_error(address, role)) } - - Ok(()) } - pub fn ensure_has_role(storage: &dyn Storage, role: &T, address: &Addr) -> StdResult<()> { - if !Self::has_role(storage, role, address) { - return Err(StdError::generic_err(format!( - "Address {} does not have role {}", - address, - role.as_ref() - ))); + pub fn ensure_has_roles(deps: &Deps, roles: &[T], address: &Addr) -> StdResult<()> { + for role in roles { + if Self::has_role(deps.storage, role, address) { + return Ok(()); + } } - Ok(()) + + Err(insufficient_permissions_error()) + } + + pub fn get_all_addresses_with_role<'a>( + storage: &'a dyn Storage, + role: &T, + ) -> Box> + 'a> { + Box::new( + HAS_ROLE + .prefix(role.as_ref()) + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|(addr, _)| addr)), + ) } } @@ -226,7 +258,7 @@ mod tests { } #[test] - fn test_give_role() { + fn test_grant_role() { let creator = Addr::unchecked("owner".to_string()); let user = Addr::unchecked("user".to_string()); let role = TestRole::RoleA; @@ -237,15 +269,15 @@ mod tests { // Create the role and set admin assert!(AccessControl::create_role(deps.as_mut().storage, &role, Some(&creator)).is_ok()); - // Admin should be able to give role - assert!(AccessControl::give_role(&mut deps.as_mut(), &env, &creator, &role, &user).is_ok()); + // Admin should be able to grant role + assert!(AccessControl::grant_role(&mut deps.as_mut(), &creator, &role, &user).is_ok()); // Ensure the user has the role assert!(AccessControl::has_role(deps.as_mut().storage, &role, &user)); } #[test] - fn test_take_role() { + fn test_revoke_role() { let creator = Addr::unchecked("owner".to_string()); let user = Addr::unchecked("user".to_string()); let role = TestRole::RoleA; @@ -256,14 +288,14 @@ mod tests { // Create the role and set admin assert!(AccessControl::create_role(deps.as_mut().storage, &role, Some(&creator)).is_ok()); - // Admin should be able to give role - assert!(AccessControl::give_role(&mut deps.as_mut(), &env, &creator, &role, &user).is_ok()); + // Admin should be able to grant role + assert!(AccessControl::grant_role(&mut deps.as_mut(), &creator, &role, &user).is_ok()); // Ensure the user has the role assert!(AccessControl::has_role(deps.as_mut().storage, &role, &user)); - // Admin should be able to take role - assert!(AccessControl::take_role(&mut deps.as_mut(), &env, &creator, &role, &user).is_ok()); + // Admin should be able to revoke role + assert!(AccessControl::revoke_role(&mut deps.as_mut(), &creator, &role, &user).is_ok()); // Ensure the user no longer has the role assert!(!AccessControl::has_role( @@ -295,8 +327,7 @@ mod tests { // Change the role admin assert!( - AccessControl::change_role_admin(deps.as_mut(), &env, &creator, &role, &new_admin) - .is_ok() + AccessControl::change_role_admin(deps.as_mut(), &creator, &role, &new_admin).is_ok() ); // Ensure the new role admin is set correctly @@ -321,10 +352,10 @@ mod tests { assert!(AccessControl::create_role(deps.as_mut().storage, &role, Some(&creator)).is_ok()); // Ensure role admin passes for the correct admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &creator, &role).is_ok()); + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &creator, &role).is_ok()); // Ensure role admin fails for someone who is not the admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &other, &role).is_err()); + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &other, &role).is_err()); } #[test] @@ -344,13 +375,13 @@ mod tests { ); // Ensure role admin passes for the correct admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &role_admin, &role).is_ok()); + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &role_admin, &role).is_ok()); - // Ensure super-admin is also role admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &creator, &role).is_ok()); + // Super-admin is not role admin + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &creator, &role).is_err()); // Ensure role admin fails for someone who is not the admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &other, &role).is_err()); + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &other, &role).is_err()); } #[test] @@ -366,10 +397,10 @@ mod tests { // Create the role and set admin assert!(AccessControl::create_role(deps.as_mut().storage, &role, None).is_ok()); - // Ensure super-admin is only role admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &creator, &role).is_ok()); + // Super-admin is not role admin + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &creator, &role).is_err()); // Ensure role admin fails for someone who is not the admin - assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &env, &other, &role).is_err()); + assert!(AccessControl::ensure_role_admin(&deps.as_ref(), &other, &role).is_err()); } } diff --git a/src/events/response_handler.rs b/src/events/response_handler.rs index 577be7d..d51b365 100644 --- a/src/events/response_handler.rs +++ b/src/events/response_handler.rs @@ -1,25 +1,29 @@ use crate::events::IntoEvent; -use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, SubMsg}; +use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Empty, Response, SubMsg}; -pub struct ResponseHandler { - response: Response, +pub struct ResponseHandler { + response: Response, } -impl ResponseHandler { - pub fn add_event(&mut self, event: T) { +impl ResponseHandler { + pub fn add_event(&mut self, event: E) { self.response.events.push(event.into_event()); } - pub fn add_msg(&mut self, msg: impl Into) { + pub fn add_message(&mut self, msg: impl Into>) { self.response.messages.push(SubMsg::new(msg)); } - pub fn into_response(self) -> Response { + pub fn add_submessage(&mut self, msg: SubMsg) { + self.response.messages.push(msg); + } + + pub fn into_response(self) -> Response { self.response } pub fn add_bank_send_msg(&mut self, to_addr: &Addr, amount: Vec) { - self.add_msg(BankMsg::Send { + self.add_message(BankMsg::Send { to_address: to_addr.to_string(), amount, }) diff --git a/src/pausing/events.rs b/src/pausing/events.rs index c387ffd..cbb454d 100644 --- a/src/pausing/events.rs +++ b/src/pausing/events.rs @@ -3,7 +3,7 @@ pub struct ContractPausedEvent<'a> { pub since_block: &'a u64, } -impl<'a> IntoEvent for ContractPausedEvent<'a> { +impl IntoEvent for ContractPausedEvent<'_> { fn event_name(&self) -> &str { "contract_paused" } diff --git a/src/permissions/mod.rs b/src/permissions/mod.rs index 30fdb04..a045f16 100644 --- a/src/permissions/mod.rs +++ b/src/permissions/mod.rs @@ -1,5 +1,8 @@ +mod private; mod superadmin; pub use crate::permissions::superadmin::{ ensure_super_admin, is_super_admin, not_super_admin_error, }; + +pub use crate::permissions::private::{ensure_private, not_self_contract_error}; diff --git a/src/permissions/private.rs b/src/permissions/private.rs new file mode 100644 index 0000000..644cfa4 --- /dev/null +++ b/src/permissions/private.rs @@ -0,0 +1,16 @@ +use cosmwasm_std::{Addr, Env}; +use cosmwasm_std::{StdError, StdResult}; + +const ERR_NOT_SELF_CONTRACT: &str = "[FET_ERR_NOT_SELF] Sender is not a self contract."; + +pub fn ensure_private(env: &Env, address: &Addr) -> StdResult<()> { + if env.contract.address != address { + return Err(not_self_contract_error()); + } + + Ok(()) +} + +pub fn not_self_contract_error() -> StdError { + StdError::generic_err(ERR_NOT_SELF_CONTRACT) +} diff --git a/src/permissions/superadmin.rs b/src/permissions/superadmin.rs index 36fa09c..a4e00de 100644 --- a/src/permissions/superadmin.rs +++ b/src/permissions/superadmin.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{StdError, StdResult}; const ERR_NOT_SUPER_ADMIN: &str = "[FET_ERR_NOT_SUPER_ADMIN] Sender is not a super-admin."; -// Check if the address is admin of the contract, everyone cannot be admin of the contract +// Check if the address is admin of the contract pub fn is_super_admin(deps: &Deps, env: &Env, address: &Addr) -> StdResult { // Check if the address is specified (opposite of the Everyone case) if let Some(admin_address) = deps