From 395a95062723bc20eade764c13094baec78a0c2c Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:43:21 +0100 Subject: [PATCH] Add Move tests to verify delegate's permissions (#1464) * chore: add identity and delegation tests * add permission tests * review comments --------- Co-authored-by: Yasir --- .../iota_identity/sources/controller.move | 177 ++++++++++++++++++ .../iota_identity/sources/identity.move | 11 +- .../sources/multicontroller.move | 28 +++ .../iota_identity/sources/permissions.move | 4 + 4 files changed, 219 insertions(+), 1 deletion(-) diff --git a/identity_iota_core/packages/iota_identity/sources/controller.move b/identity_iota_core/packages/iota_identity/sources/controller.move index 4d26a742f..73c2d0f22 100644 --- a/identity_iota_core/packages/iota_identity/sources/controller.move +++ b/identity_iota_core/packages/iota_identity/sources/controller.move @@ -147,4 +147,181 @@ module iota_identity::controller { } = token; object::delete(id); } +} + +#[test_only] +module iota_identity::controller_tests { + use iota::test_scenario; + use iota_identity::controller::{Self, ControllerCap, ECannotDelegate, EInvalidPermissions}; + use iota_identity::permissions; + use iota_identity::multicontroller::{Self, Multicontroller}; + + #[test, expected_failure(abort_code = ECannotDelegate)] + fun test_only_delegatable_controllers_can_create_delegation_tokens() { + let owner = @0x1; + let mut scenario = test_scenario::begin(owner); + + let non_delegatable = controller::new(false, scenario.ctx()); + let delegation_token = non_delegatable.delegate(scenario.ctx()); + + delegation_token.delete(); + non_delegatable.delete(); + scenario.end(); + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_create_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_create_proposal()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_execute_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_execute_proposal()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.execute_proposal<_, u64>( + &delegation_token, + proposal_id, + scenario.ctx(), + ).unwrap(); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_approve_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_approve_proposal()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.approve_proposal<_, u64>( + &delegation_token, + proposal_id, + ); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_remove_approval_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_remove_approval()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.remove_approval<_, u64>( + &delegation_token, + proposal_id, + ); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_delete_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_remove_approval()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.remove_approval<_, u64>( + &delegation_token, + proposal_id, + ); + + multicontroller.delete_proposal<_, u64>( + &delegation_token, + proposal_id, + ); + + abort(0) + } } \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move index e57dd213b..e7da1123c 100644 --- a/identity_iota_core/packages/iota_identity/sources/identity.move +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -169,7 +169,7 @@ module iota_identity::identity { } } - /// Proposes the deativation of the DID Document contained in this `Identity`. + /// Proposes the deactivation of the DID Document contained in this `Identity`. public fun propose_deactivation( self: &mut Identity, cap: &DelegationToken, @@ -481,6 +481,15 @@ module iota_identity::identity { self.did_doc.execute_proposal(cap, proposal_id, ctx) } + /// Deletes an `Identity`'s proposal. Only proposals with no votes can be deleted. + public fun delete_proposal( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + ) { + self.did_doc.delete_proposal<_, T>(cap, proposal_id); + } + /// revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. public fun revoke_token(self: &mut Identity, cap: &ControllerCap, deny_id: ID) { self.did_doc.revoke_token(cap, deny_id); diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move index da88fd270..540435ac1 100644 --- a/identity_iota_core/packages/iota_identity/sources/multicontroller.move +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -13,6 +13,7 @@ module iota_identity::multicontroller { const EExpiredProposal: u64 = 4; const ENotVotedYet: u64 = 5; const EProposalNotFound: u64 = 6; + const ECannotDelete: u64 = 7; /// Shares control of a value `V` with multiple entities called controllers. public struct Multicontroller has store { @@ -242,6 +243,33 @@ module iota_identity::multicontroller { proposal.votes = proposal.votes - vp; } + /// Removes a proposal no one has voted for. + public fun delete_proposal( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ) { + cap.assert_has_permission(permissions::can_delete_proposal()); + + let proposal = multi.proposals.remove>(proposal_id); + assert!(proposal.votes == 0, ECannotDelete); + + let Proposal { + id, + votes: _, + voters: _, + expiration_epoch: _, + action: _, + } = proposal; + + id.delete(); + + let (present, i) = multi.active_proposals.index_of(&proposal_id); + assert!(present, EProposalNotFound); + + multi.active_proposals.remove(i); + } + /// Returns a reference to `multi`'s value. public fun value(multi: &Multicontroller): &V { &multi.controlled_value diff --git a/identity_iota_core/packages/iota_identity/sources/permissions.move b/identity_iota_core/packages/iota_identity/sources/permissions.move index 152e7782a..35dc601fc 100644 --- a/identity_iota_core/packages/iota_identity/sources/permissions.move +++ b/identity_iota_core/packages/iota_identity/sources/permissions.move @@ -17,4 +17,8 @@ module iota_identity::permissions { public fun can_delete_proposal(): u32 { CAN_DELETE_PROPOSAL } public fun can_remove_approval(): u32 { CAN_REMOVE_APPROVAL } public fun all(): u32 { ALL_PERMISSIONS } + /// Negate a permission + public fun not(permission: u32): u32 { + permission ^ all() + } } \ No newline at end of file