feat(standards): extend Authority with role assignment map#3072
Conversation
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good. The main comment is about removal of the default role.
| # => [mapped_role, default_role, 0, 0] | ||
|
|
||
| dup eq.0 | ||
| if.true |
There was a problem hiding this comment.
| # => [mapped_role, default_role, 0, 0] | |
| dup eq.0 | |
| if.true | |
| # => [mapped_role, default_role, 0, 0] | |
| dup eq.0 | |
| # => [has_no_mapped_role, default_role, 0, 0] | |
| if.true |
Nit: I would indicate what the comparison of a role to zero means.
| # No per-procedure role; fall back to the default. | ||
| drop | ||
| # => [default_role, 0, 0] | ||
|
|
||
| dup eq.0 | ||
| if.true | ||
| # Zero default role — defer to the Ownable2Step owner. | ||
| drop drop drop | ||
| # => [] | ||
| exec.ownable2step::assert_sender_is_owner | ||
| # => [] | ||
| else | ||
| # Non-zero default role — require it. | ||
| # => [default_role, 0, 0] | ||
| exec.rbac::assert_sender_has_role | ||
| # => [0, 0] | ||
| drop drop | ||
| # => [] | ||
| end |
There was a problem hiding this comment.
In the interest of simplification, I would remove the default_role and only have a two step logic: If a procedure is configured, assert the sender has the role, if not, require the owner.
| static AUTHORITY_ROLE_MAP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| { | ||
| StorageSlotName::new("miden::standards::access::authority::role_map") | ||
| .expect("storage slot name should be valid") | ||
| }); |
There was a problem hiding this comment.
Nit: Maybe we can capture the "procedure root to role" mapping in the slot name somehow? Something like procedure_roles?
partylikeits1983
left a comment
There was a problem hiding this comment.
Looks good! I agree with Philipp, I think the default_role should be removed. Essentially if a procedure is configured with some role, check if the sender has that role, if not require the owner.
Also left some nits regarding masm formatting, but these might be removed if / once default_role is removed.
| # Resolve the calling procedure's root and look up its assigned role in the map. | ||
| padw caller | ||
| # => [CALLER_ROOT, default_role, 0, 0] | ||
| push.AUTHORITY_ROLE_MAP_SLOT[0..2] | ||
| # => [slot_suffix, slot_prefix, CALLER_ROOT, default_role, 0, 0] | ||
| exec.active_account::get_map_item | ||
| # => [mapped_role, 0, 0, 0, default_role, 0, 0] |
There was a problem hiding this comment.
formatting:
| # Resolve the calling procedure's root and look up its assigned role in the map. | |
| padw caller | |
| # => [CALLER_ROOT, default_role, 0, 0] | |
| push.AUTHORITY_ROLE_MAP_SLOT[0..2] | |
| # => [slot_suffix, slot_prefix, CALLER_ROOT, default_role, 0, 0] | |
| exec.active_account::get_map_item | |
| # => [mapped_role, 0, 0, 0, default_role, 0, 0] | |
| # Resolve the calling procedure's root and look up its assigned role in the map. | |
| padw caller | |
| # => [CALLER_ROOT, default_role, 0, 0] | |
| push.AUTHORITY_ROLE_MAP_SLOT[0..2] | |
| # => [slot_suffix, slot_prefix, CALLER_ROOT, default_role, 0, 0] | |
| exec.active_account::get_map_item | |
| # => [mapped_role, 0, 0, 0, default_role, 0, 0] |
| # Per-procedure role found — require it. | ||
| # => [mapped_role, default_role, 0, 0] | ||
| exec.rbac::assert_sender_has_role | ||
| # => [default_role, 0, 0] | ||
| drop drop drop | ||
| # => [] |
There was a problem hiding this comment.
I think the wording of this comment is a bit weird, can we update it to something like this? Also line formatting is off.
| # Per-procedure role found — require it. | |
| # => [mapped_role, default_role, 0, 0] | |
| exec.rbac::assert_sender_has_role | |
| # => [default_role, 0, 0] | |
| drop drop drop | |
| # => [] | |
| # Assert sender has role | |
| # => [mapped_role, default_role, 0, 0] | |
| exec.rbac::assert_sender_has_role | |
| # => [default_role, 0, 0] | |
| drop drop drop | |
| # => [] |
| # Zero default role — defer to the Ownable2Step owner. | ||
| drop drop drop | ||
| # => [] | ||
| exec.ownable2step::assert_sender_is_owner | ||
| # => [] |
There was a problem hiding this comment.
| # Zero default role — defer to the Ownable2Step owner. | |
| drop drop drop | |
| # => [] | |
| exec.ownable2step::assert_sender_is_owner | |
| # => [] | |
| # Zero default role, defer to the Ownable2Step owner. | |
| drop drop drop | |
| # => [] | |
| exec.ownable2step::assert_sender_is_owner | |
| # => [] |
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I agree with @PhilippGackstatter and @partylikeits1983 that we could probably get rid of the default role and fall back to the owner immediately.
PhilippGackstatter
left a comment
There was a problem hiding this comment.
LGTM - left a few non-blocking nits.
| fn config_word(&self) -> Word { | ||
| let discriminant = match self { | ||
| Authority::AuthControlled => AUTH_CONTROLLED, | ||
| Authority::OwnerControlled => OWNER_CONTROLLED, | ||
| Authority::RbacControlled { .. } => RBAC_CONTROLLED, | ||
| }; | ||
| Word::new([Felt::from(discriminant), Felt::ZERO, Felt::ZERO, Felt::ZERO]) |
There was a problem hiding this comment.
Nit: I'd maybe extract this logic into a separate fn as_u8(&self) -> u8 as it may become useful in other places.
| let slot = storage | ||
| .slots() | ||
| .iter() | ||
| .find(|slot| slot.name().id() == AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.id()) | ||
| .ok_or(AuthorityError::InvalidProcedureRolesSlot)?; |
There was a problem hiding this comment.
Nit: Should we align the name of the error with MissingStorageSlot and call it MissingProcedureRolesSlot since it's missing in this case, not invalid? Alternatively, we reuse and adapt MissingStorageSlot for this use case.
| /// Encodes the authority configuration value slot word: `[authority, 0, 0, 0]`. | ||
| fn config_word(&self) -> Word { |
There was a problem hiding this comment.
I think this could be just to_word, now that it encodes only the discriminant.
| @@ -38,6 +47,9 @@ const ERR_PAUSABLE_IS_PAUSED: MasmError = MasmError::from_static_str("the contra | |||
|
|
|||
| const ERR_SENDER_NOT_OWNER: MasmError = MasmError::from_static_str("note sender is not the owner"); | |||
|
|
|||
| const ERR_SENDER_LACKS_ROLE: MasmError = | |||
| MasmError::from_static_str("note sender does not hold the required role"); | |||
|
|
|||
There was a problem hiding this comment.
These can all be imported as miden_standards::errors::standards::{ERR_PAUSABLE_IS_PAUSED, ERR_SENDER_LACKS_ROLE, ERR_SENDER_NOT_OWNER}
| let mut mock_chain = builder.build()?; | ||
| mock_chain.prove_next_block()?; | ||
|
|
||
| execute_note_on_faucet(&mut mock_chain, faucet.id(), &grant_pauser).await?; | ||
| execute_note_on_faucet(&mut mock_chain, faucet.id(), &grant_unpauser).await?; | ||
|
|
||
| // PAUSER pauses → the faucet records the paused state. | ||
| execute_note_on_faucet(&mut mock_chain, faucet.id(), &pause_note).await?; | ||
| assert!(is_faucet_paused(&mock_chain, faucet.id())?); | ||
|
|
||
| // UNPAUSER unpauses → the faucet records the unpaused state. | ||
| execute_note_on_faucet(&mut mock_chain, faucet.id(), &unpause_note).await?; | ||
| assert!(!is_faucet_paused(&mock_chain, faucet.id())?); |
There was a problem hiding this comment.
This is a super clean test 🙌
Closes #3070.