Skip to main content

miden_standards/account/access/
rbac.rs

1use alloc::vec;
2
3use miden_protocol::account::component::{
4    AccountComponentCode,
5    AccountComponentMetadata,
6    SchemaType,
7    StorageSchema,
8    StorageSlotSchema,
9};
10use miden_protocol::account::{
11    AccountComponent,
12    AccountComponentName,
13    StorageMap,
14    StorageSlot,
15    StorageSlotName,
16};
17use miden_protocol::utils::sync::LazyLock;
18
19use crate::account::account_component_code;
20
21account_component_code!(RBAC_CODE, "access/rbac.masl");
22
23static ROLE_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
24    StorageSlotName::new("miden::standards::access::rbac::role_config")
25        .expect("storage slot name should be valid")
26});
27static ROLE_MEMBERSHIP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
28    StorageSlotName::new("miden::standards::access::rbac::role_membership")
29        .expect("storage slot name should be valid")
30});
31
32/// Role-based access control (RBAC) for account components.
33///
34/// RBAC provides fine-grained access control on top of [`Ownable2Step`]. Instead of having
35/// one account holding every privilege, privileges are split into named roles (for example
36/// `MINTER`, `BURNER`, `PAUSER`), and each procedure is guarded against the caller's role
37/// membership. It allows role assignment with domain isolation to minimize the scope of
38/// damage from a compromised role.
39///
40/// ## Relation to [`Ownable2Step`]
41///
42/// RBAC is a superset of [`Ownable2Step`] and depends on it: the top-level authority is
43/// the [`Ownable2Step`] owner of the account. Build the pair via
44/// [`AccessControl::Rbac`][crate::account::access::AccessControl::Rbac] passed to
45/// [`AccountBuilder::with_components`][miden_protocol::account::AccountBuilder::with_components].
46/// This avoids duplicated state, duplicated 2-step transfer logic, and duplicated notes
47/// for owner transfers. If you only need single-account control, use [`Ownable2Step`]
48/// alone.
49///
50/// [`Ownable2Step`]: crate::account::access::Ownable2Step
51///
52/// ## Owner management
53///
54/// The owner can grant and revoke any role, configure the delegated admin of any role via
55/// `set_role_admin`, and transfer or renounce its own position. Owner transfer and
56/// renouncement go through [`Ownable2Step`] (`transfer_ownership`, `accept_ownership`,
57/// `renounce_ownership`).
58///
59/// ## Role hierarchy
60///
61/// Every role may optionally have a delegated admin role. Accounts holding a role's admin
62/// role are authorized to grant and revoke that role without going through the owner.
63/// For example, accounts holding `MINTER_ADMIN` can manage the `MINTER` role but have no
64/// authority over `BURNER` or `PAUSER`. This lets responsibilities be distributed so that
65/// compromise of one domain does not spill into the others.
66///
67/// Combined with owner renouncement, this supports a fully decentralized configuration:
68/// once every role has its own admin role populated, the owner can renounce and the
69/// system continues to operate with each role managed only by its designated admin role.
70///
71/// The delegated admin of a role can itself be any role, including one that it admins.
72/// Circular relationships are possible but should be designed with care, since each role
73/// can then revoke the other.
74///
75/// ## Role semantics
76///
77/// A role is considered to exist when it has at least one member. Granting the first
78/// member creates the role; revoking the last member removes it. As a consequence,
79/// `set_role_admin(A, B)` stores the admin relationship in storage but does not make role
80/// `A` exist until a member is granted. Once the last member of `A` is revoked,
81/// `get_role_member_count(A)` returns `0`, though the admin configuration is retained and
82/// will apply the next time a member is granted.
83///
84/// ## Membership lookup
85///
86/// `has_role` procedure is the primary guard used by procedures that assert the caller's
87/// role membership. `get_role_member_count` returns the number of accounts holding a role.
88///
89/// ## Role symbol format
90///
91/// A [`RoleSymbol`] encodes up to 12 uppercase ASCII characters with underscores into a
92/// single field element using the same packing as the token symbol type. Examples:
93/// `MINTER`, `MINTER_ADMIN`, `PAUSER`. The zero field element is reserved and cannot be
94/// used as a role symbol; attempting to do so panics with `ERR_ROLE_SYMBOL_ZERO`.
95///
96/// ## Usage
97///
98/// Guarding a procedure in MASM so that only members of `MINTER` can call it:
99///
100/// ```text
101/// pub proc mint
102///     push.MINTER_ROLE_SYMBOL
103///     exec.::miden::standards::access::rbac::assert_sender_has_role
104///     # add mint logic
105/// end
106/// ```
107///
108/// [`RoleSymbol`]: miden_protocol::account::RoleSymbol
109#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct RoleBasedAccessControl;
111
112impl RoleBasedAccessControl {
113    pub const NAME: &'static str = "miden::standards::components::access::rbac";
114
115    /// Returns the canonical [`AccountComponentName`] of this component.
116    pub const fn name() -> AccountComponentName {
117        AccountComponentName::from_static_str(Self::NAME)
118    }
119
120    /// Returns the [`AccountComponentCode`] of this component.
121    pub fn code() -> &'static AccountComponentCode {
122        &RBAC_CODE
123    }
124
125    /// Returns an empty RBAC component. Roles are populated at runtime via the
126    /// `grant_role`, `set_role_admin`, etc. procedures exposed by the component.
127    pub fn empty() -> Self {
128        Self
129    }
130
131    /// Returns the storage slot name for the per-role config map.
132    pub fn role_config_slot() -> &'static StorageSlotName {
133        &ROLE_CONFIG_SLOT_NAME
134    }
135
136    /// Returns the storage slot name for the per-role membership map.
137    pub fn role_membership_slot() -> &'static StorageSlotName {
138        &ROLE_MEMBERSHIP_SLOT_NAME
139    }
140
141    /// Returns the schema entry for the per-role config map.
142    pub fn role_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
143        (
144            Self::role_config_slot().clone(),
145            StorageSlotSchema::map(
146                "Per-role RBAC configuration (member count and delegated admin role)",
147                SchemaType::role_symbol(),
148                SchemaType::native_word(),
149            ),
150        )
151    }
152
153    /// Returns the schema entry for the per-role membership map.
154    pub fn role_membership_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
155        (
156            Self::role_membership_slot().clone(),
157            StorageSlotSchema::map(
158                "Role membership flag indexed by role symbol and account ID",
159                SchemaType::native_word(),
160                SchemaType::native_word(),
161            ),
162        )
163    }
164
165    /// Returns the [`AccountComponentMetadata`] describing this component.
166    pub fn component_metadata() -> AccountComponentMetadata {
167        let storage_schema = StorageSchema::new(vec![
168            Self::role_config_slot_schema(),
169            Self::role_membership_slot_schema(),
170        ])
171        .expect("storage schema should be valid");
172
173        AccountComponentMetadata::new(Self::NAME)
174            .with_description("Role-based access control component")
175            .with_storage_schema(storage_schema)
176    }
177}
178
179impl From<RoleBasedAccessControl> for AccountComponent {
180    fn from(_rbac: RoleBasedAccessControl) -> Self {
181        let role_config_slot = StorageSlot::with_map(
182            RoleBasedAccessControl::role_config_slot().clone(),
183            StorageMap::with_entries(vec![]).expect("empty role config map should be valid"),
184        );
185        let role_membership_slot = StorageSlot::with_map(
186            RoleBasedAccessControl::role_membership_slot().clone(),
187            StorageMap::with_entries(vec![]).expect("empty role membership map should be valid"),
188        );
189
190        AccountComponent::new(
191            RoleBasedAccessControl::code().clone(),
192            vec![role_config_slot, role_membership_slot],
193            RoleBasedAccessControl::component_metadata(),
194        )
195        .expect("RBAC component should satisfy the requirements of a valid account component")
196    }
197}