diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index a0a33516473..94feb92a082 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -215,3 +215,87 @@ fn role_permissions_unified() { "permission tokens for role aren't deduplicated" ); } + +#[test] +fn grant_revoke_role_permissions() -> Result<()> { + let chain_id = ChainId::from("0"); + + let (_rt, _peer, test_client) = ::new().with_port(11_245).start_with_runtime(); + wait_for_genesis_committed(&vec![test_client.clone()], 0); + + let alice_id = AccountId::from_str("alice@wonderland")?; + let mouse_id = AccountId::from_str("mouse@wonderland")?; + + // Registering Mouse + let mouse_key_pair = KeyPair::generate(); + let register_mouse = Register::account(Account::new( + mouse_id.clone(), + mouse_key_pair.public_key().clone(), + )); + test_client.submit_blocking(register_mouse)?; + + // Registering role + let role_id = RoleId::from_str("ACCESS_TO_MOUSE_METADATA")?; + let role = Role::new(role_id.clone()); + let register_role = Register::role(role); + test_client.submit_blocking(register_role)?; + + // Transfer domain ownership to Mouse + let domain_id = DomainId::from_str("wonderland")?; + let transfer_domain = Transfer::domain(alice_id.clone(), domain_id, mouse_id.clone()); + test_client.submit_blocking(transfer_domain)?; + + // Mouse grants role to Alice + let grant_role = Grant::role(role_id.clone(), alice_id.clone()); + let grant_role_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) + .with_instructions([grant_role]) + .sign(&mouse_key_pair); + test_client.submit_transaction_blocking(&grant_role_tx)?; + + let set_key_value = SetKeyValue::account( + mouse_id.clone(), + Name::from_str("key").expect("Valid"), + Value::String("value".to_owned()), + ); + let permission = PermissionToken::new( + "CanSetKeyValueInUserAccount".parse()?, + &json!({ "account_id": mouse_id }), + ); + let grant_role_permission = Grant::role_permission(permission.clone(), role_id.clone()); + let revoke_role_permission = Revoke::role_permission(permission.clone(), role_id.clone()); + + // Alice can't modify Mouse's metadata without proper permission token + let found_permissions = test_client + .request(FindPermissionTokensByAccountId::new(alice_id.clone()))? + .collect::>>()?; + assert!(!found_permissions.contains(&permission)); + let _ = test_client + .submit_blocking(set_key_value.clone()) + .expect_err("shouldn't be able to modify metadata"); + + // Alice can modify Mouse's metadata after permission token is granted to role + let grant_role_permission_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) + .with_instructions([grant_role_permission]) + .sign(&mouse_key_pair); + test_client.submit_transaction_blocking(&grant_role_permission_tx)?; + let found_permissions = test_client + .request(FindPermissionTokensByAccountId::new(alice_id.clone()))? + .collect::>>()?; + assert!(found_permissions.contains(&permission)); + test_client.submit_blocking(set_key_value.clone())?; + + // Alice can't modify Mouse's metadata after permission token is removed from role + let revoke_role_permission_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) + .with_instructions([revoke_role_permission]) + .sign(&mouse_key_pair); + test_client.submit_transaction_blocking(&revoke_role_permission_tx)?; + let found_permissions = test_client + .request(FindPermissionTokensByAccountId::new(alice_id.clone()))? + .collect::>>()?; + assert!(!found_permissions.contains(&permission)); + let _ = test_client + .submit_blocking(set_key_value.clone()) + .expect_err("shouldn't be able to modify metadata"); + + Ok(()) +} diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index 48ec36119ad..435df040c27 100644 Binary files a/configs/swarm/executor.wasm and b/configs/swarm/executor.wasm differ diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index 437ef7c1c91..3280ccb29b4 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -208,6 +208,7 @@ impl Execute for GrantBox { match self { Self::PermissionToken(sub_isi) => sub_isi.execute(authority, wsv), Self::Role(sub_isi) => sub_isi.execute(authority, wsv), + Self::RolePermissionToken(sub_isi) => sub_isi.execute(authority, wsv), } } } @@ -218,6 +219,7 @@ impl Execute for RevokeBox { match self { Self::PermissionToken(sub_isi) => sub_isi.execute(authority, wsv), Self::Role(sub_isi) => sub_isi.execute(authority, wsv), + Self::RolePermissionToken(sub_isi) => sub_isi.execute(authority, wsv), } } } diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index 44ae2f2eb2e..1b768138e14 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -174,6 +174,66 @@ pub mod isi { } } + impl Execute for Grant { + #[metrics(+"grant_role_permission")] + fn execute(self, _authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { + let role_id = self.destination_id; + let permission_token = self.object; + let permission_token_id = permission_token.definition_id.clone(); + + if !wsv + .permission_token_schema() + .token_ids + .contains(&permission_token_id) + { + return Err(FindError::PermissionToken(permission_token_id).into()); + } + + let Some(role) = wsv.world.roles.get_mut(&role_id) else { + return Err(FindError::Role(role_id).into()); + }; + + if !role.permissions.insert(permission_token.clone()) { + return Err(RepetitionError { + instruction_type: InstructionType::Grant, + id: permission_token.definition_id.into(), + } + .into()); + } + + wsv.emit_events(Some(RoleEvent::PermissionAdded(RolePermissionChanged { + role_id, + permission_token_id, + }))); + + Ok(()) + } + } + + impl Execute for Revoke { + #[metrics(+"grant_role_permission")] + fn execute(self, _authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { + let role_id = self.destination_id; + let permission_token = self.object; + let permission_token_id = permission_token.definition_id.clone(); + + let Some(role) = wsv.world.roles.get_mut(&role_id) else { + return Err(FindError::Role(role_id).into()); + }; + + if !role.permissions.remove(&permission_token) { + return Err(FindError::PermissionToken(permission_token_id).into()); + } + + wsv.emit_events(Some(RoleEvent::PermissionRemoved(RolePermissionChanged { + role_id, + permission_token_id, + }))); + + Ok(()) + } + } + impl Execute for SetParameter { #[metrics(+"set_parameter")] fn execute(self, _authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { diff --git a/data_model/src/events/data/events.rs b/data_model/src/events/data/events.rs index c08b9e504c0..38eda004fe4 100644 --- a/data_model/src/events/data/events.rs +++ b/data_model/src/events/data/events.rs @@ -246,7 +246,11 @@ mod role { /// [`PermissionToken`]s with particular [`Id`](crate::permission::token::PermissionTokenId) /// were removed from the role. #[has_origin(permission_removed => &permission_removed.role_id)] - PermissionRemoved(PermissionRemoved), + PermissionRemoved(RolePermissionChanged), + /// [`PermissionToken`]s with particular [`Id`](crate::permission::token::PermissionTokenId) + /// were removed from the role. + #[has_origin(permission_added => &permission_added.role_id)] + PermissionAdded(RolePermissionChanged), } } @@ -254,7 +258,7 @@ mod role { pub mod model { use super::*; - /// Information about permissions removed from [`Role`] + /// Depending on the wrapping event, [`RolePermissionChanged`] role represents the added or removed role's permission #[derive( Debug, Clone, @@ -271,7 +275,7 @@ mod role { )] #[getset(get = "pub")] #[ffi_type] - pub struct PermissionRemoved { + pub struct RolePermissionChanged { pub role_id: RoleId, // TODO: Skipped temporarily because of FFI #[getset(skip)] @@ -655,7 +659,7 @@ pub mod prelude { executor::{ExecutorEvent, ExecutorFilter}, peer::{PeerEvent, PeerEventFilter, PeerFilter}, permission::PermissionTokenSchemaUpdateEvent, - role::{PermissionRemoved, RoleEvent, RoleEventFilter, RoleFilter}, + role::{RoleEvent, RoleEventFilter, RoleFilter, RolePermissionChanged}, trigger::{ TriggerEvent, TriggerEventFilter, TriggerFilter, TriggerNumberOfExecutionsChanged, }, diff --git a/data_model/src/isi.rs b/data_model/src/isi.rs index 178f1617bf6..bedb511c7d6 100644 --- a/data_model/src/isi.rs +++ b/data_model/src/isi.rs @@ -173,8 +173,10 @@ impl_instruction! { Transfer, Grant, Grant, + Grant, Revoke, Revoke, + Revoke, SetParameter, NewParameter, Upgrade, @@ -971,6 +973,16 @@ mod transparent { } } + impl Grant { + /// Constructs a new [`Grant`] for giving a [`PermissionToken`] to [`Role`]. + pub fn role_permission(permission_token: PermissionToken, to: RoleId) -> Self { + Self { + object: permission_token, + destination_id: to, + } + } + } + impl_display! { Grant where @@ -985,7 +997,8 @@ mod transparent { impl_into_box! { Grant | - Grant + Grant | + Grant => GrantBox => InstructionBox[Grant], => GrantBoxRef<'a> => InstructionBoxRef<'a>[Grant] } @@ -1021,6 +1034,16 @@ mod transparent { } } + impl Revoke { + /// Constructs a new [`Revoke`] for removing a [`PermissionToken`] from [`Role`]. + pub fn role_permission(permission_token: PermissionToken, from: RoleId) -> Self { + Self { + object: permission_token, + destination_id: from, + } + } + } + impl_display! { Revoke where @@ -1035,7 +1058,8 @@ mod transparent { impl_into_box! { Revoke | - Revoke + Revoke | + Revoke => RevokeBox => InstructionBox[Revoke], => RevokeBoxRef<'a> => InstructionBoxRef<'a>[Revoke] } @@ -1327,6 +1351,8 @@ isi_box! { PermissionToken(Grant), /// Grant [`Role`] to [`Account`]. Role(Grant), + /// Grant [`PermissionToken`] to [`Role`]. + RolePermissionToken(Grant), } } @@ -1342,6 +1368,8 @@ isi_box! { PermissionToken(Revoke), /// Revoke [`Role`] from [`Account`]. Role(Revoke), + /// Revoke [`PermissionToken`] from [`Account`]. + RolePermissionToken(Revoke), } } diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 9ed47460625..605ada68ec6 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -135,9 +135,11 @@ mod seal { Grant, Grant, + Grant, Revoke, Revoke, + Revoke, SetParameter, NewParameter, diff --git a/data_model/src/visit.rs b/data_model/src/visit.rs index fcae7d5b2d5..6987fa5c59d 100644 --- a/data_model/src/visit.rs +++ b/data_model/src/visit.rs @@ -143,10 +143,12 @@ pub trait Visit { // Visit GrantBox visit_grant_account_permission(&Grant), visit_grant_account_role(&Grant), + visit_grant_role_permission(&Grant), // Visit RevokeBox visit_revoke_account_permission(&Revoke), visit_revoke_account_role(&Revoke), + visit_revoke_role_permission(&Revoke), } } @@ -389,6 +391,7 @@ pub fn visit_grant(visitor: &mut V, authority: &AccountId, is match isi { GrantBox::PermissionToken(obj) => visitor.visit_grant_account_permission(authority, obj), GrantBox::Role(obj) => visitor.visit_grant_account_role(authority, obj), + GrantBox::RolePermissionToken(obj) => visitor.visit_grant_role_permission(authority, obj), } } @@ -396,6 +399,7 @@ pub fn visit_revoke(visitor: &mut V, authority: &AccountId, i match isi { RevokeBox::PermissionToken(obj) => visitor.visit_revoke_account_permission(authority, obj), RevokeBox::Role(obj) => visitor.visit_revoke_account_role(authority, obj), + RevokeBox::RolePermissionToken(obj) => visitor.visit_revoke_role_permission(authority, obj), } } @@ -448,6 +452,8 @@ leaf_visitors! { visit_unregister_role(&Unregister), visit_grant_account_role(&Grant), visit_revoke_account_role(&Revoke), + visit_grant_role_permission(&Grant), + visit_revoke_role_permission(&Revoke), visit_register_trigger(&Register>), visit_unregister_trigger(&Unregister>), visit_mint_trigger_repetitions(&Mint>), diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 5f85aac7b93..b3e0b69cc87 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -1896,6 +1896,18 @@ } ] }, + "Grant": { + "Struct": [ + { + "name": "object", + "type": "PermissionToken" + }, + { + "name": "destination_id", + "type": "RoleId" + } + ] + }, "Grant": { "Struct": [ { @@ -1919,6 +1931,11 @@ "tag": "Role", "discriminant": 1, "type": "Grant" + }, + { + "tag": "RolePermissionToken", + "discriminant": 2, + "type": "Grant" } ] }, @@ -2924,18 +2941,6 @@ } ] }, - "PermissionRemoved": { - "Struct": [ - { - "name": "role_id", - "type": "RoleId" - }, - { - "name": "permission_token_id", - "type": "Name" - } - ] - }, "PermissionToken": { "Struct": [ { @@ -3577,6 +3582,18 @@ } ] }, + "Revoke": { + "Struct": [ + { + "name": "object", + "type": "PermissionToken" + }, + { + "name": "destination_id", + "type": "RoleId" + } + ] + }, "Revoke": { "Struct": [ { @@ -3600,6 +3617,11 @@ "tag": "Role", "discriminant": 1, "type": "Revoke" + }, + { + "tag": "RolePermissionToken", + "discriminant": 2, + "type": "Revoke" } ] }, @@ -3630,7 +3652,12 @@ { "tag": "PermissionRemoved", "discriminant": 2, - "type": "PermissionRemoved" + "type": "RolePermissionChanged" + }, + { + "tag": "PermissionAdded", + "discriminant": 3, + "type": "RolePermissionChanged" } ] }, @@ -3647,6 +3674,10 @@ { "tag": "ByPermissionRemoved", "discriminant": 2 + }, + { + "tag": "ByPermissionAdded", + "discriminant": 3 } ] }, @@ -3670,6 +3701,18 @@ } ] }, + "RolePermissionChanged": { + "Struct": [ + { + "name": "role_id", + "type": "RoleId" + }, + { + "name": "permission_token_id", + "type": "Name" + } + ] + }, "Schedule": { "Struct": [ { diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index fa572e164f6..83de566925e 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -260,7 +260,7 @@ types!( PeerEventFilter, PeerFilter, PeerId, - PermissionRemoved, + RolePermissionChanged, PermissionToken, PermissionTokenSchema, PermissionTokenSchemaUpdateEvent, diff --git a/smart_contract/executor/derive/src/default.rs b/smart_contract/executor/derive/src/default.rs index 67a7e6cad3d..856ae027138 100644 --- a/smart_contract/executor/derive/src/default.rs +++ b/smart_contract/executor/derive/src/default.rs @@ -155,6 +155,8 @@ pub fn impl_derive_visit(emitter: &mut Emitter, input: &syn2::DeriveInput) -> To "fn visit_unregister_role(operation: &Unregister)", "fn visit_grant_account_role(operation: &Grant)", "fn visit_revoke_account_role(operation: &Revoke)", + "fn visit_grant_role_permission(operation: &Grant)", + "fn visit_revoke_role_permission(operation: &Revoke)", "fn visit_register_trigger(operation: &Register>)", "fn visit_unregister_trigger(operation: &Unregister>)", "fn visit_mint_trigger_repetitions(operation: &Mint>)", diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index 3851b1e03ad..280ef2bcb35 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -34,7 +34,8 @@ pub use parameter::{visit_new_parameter, visit_set_parameter}; pub use peer::{visit_register_peer, visit_unregister_peer}; pub use permission_token::{visit_grant_account_permission, visit_revoke_account_permission}; pub use role::{ - visit_grant_account_role, visit_register_role, visit_revoke_account_role, visit_unregister_role, + visit_grant_account_role, visit_grant_role_permission, visit_register_role, + visit_revoke_account_role, visit_revoke_role_permission, visit_unregister_role, }; pub use trigger::{ visit_burn_trigger_repetitions, visit_execute_trigger, visit_mint_trigger_repetitions, @@ -1288,7 +1289,7 @@ pub mod role { use super::*; - macro_rules! impl_validate { + macro_rules! impl_validate_grant_remove_account_role { ($executor:ident, $isi:ident, $authority:ident, $method:ident) => { let role_id = $isi.object(); @@ -1326,6 +1327,35 @@ pub mod role { }; } + macro_rules! impl_validate_grant_revoke_role_permission { + ($executor:ident, $isi:ident, $authority:ident, $method:ident, $isi_type:ty) => { + let role_id = $isi.destination_id().clone(); + let token = $isi.object(); + + if let Ok(any_token) = AnyPermissionToken::try_from(token) { + let token = PermissionToken::from(any_token.clone()); + let isi = <$isi_type>::role_permission(token, role_id); + if is_genesis($executor) { + execute!($executor, isi); + } + if let Err(error) = permission::ValidateGrantRevoke::$method( + &any_token, + $authority, + $executor.block_height(), + ) { + deny!($executor, error); + } + + execute!($executor, isi); + } + + deny!( + $executor, + ValidationFail::NotPermitted(format!("{token:?}: Unknown permission token")) + ); + }; + } + #[allow(clippy::needless_pass_by_value)] pub fn visit_register_role( executor: &mut V, @@ -1383,7 +1413,7 @@ pub mod role { authority: &AccountId, isi: &Grant, ) { - impl_validate!(executor, isi, authority, validate_grant); + impl_validate_grant_remove_account_role!(executor, isi, authority, validate_grant); } pub fn visit_revoke_account_role( @@ -1391,7 +1421,23 @@ pub mod role { authority: &AccountId, isi: &Revoke, ) { - impl_validate!(executor, isi, authority, validate_revoke); + impl_validate_grant_remove_account_role!(executor, isi, authority, validate_revoke); + } + + pub fn visit_grant_role_permission( + executor: &mut V, + authority: &AccountId, + isi: &Grant, + ) { + impl_validate_grant_revoke_role_permission!(executor, isi, authority, validate_grant, Grant); + } + + pub fn visit_revoke_role_permission( + executor: &mut V, + authority: &AccountId, + isi: &Revoke, + ) { + impl_validate_grant_revoke_role_permission!(executor, isi, authority, validate_revoke, Revoke); } }