Skip to main content

miden_standards/account/access/
rbac.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec;
3use alloc::vec::Vec;
4
5use miden_protocol::account::component::{
6    AccountComponentCode,
7    AccountComponentMetadata,
8    SchemaType,
9    StorageSchema,
10    StorageSlotSchema,
11};
12use miden_protocol::account::{
13    AccountComponent,
14    AccountComponentName,
15    AccountId,
16    RoleSymbol,
17    StorageMap,
18    StorageMapKey,
19    StorageSlot,
20    StorageSlotName,
21};
22use miden_protocol::utils::sync::LazyLock;
23use miden_protocol::{Felt, Word};
24
25use crate::account::account_component_code;
26
27account_component_code!(RBAC_CODE, "miden-standards-access-rbac.masp");
28
29static ROLE_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
30    StorageSlotName::new("miden::standards::access::rbac::role_config")
31        .expect("storage slot name should be valid")
32});
33static ROLE_MEMBERSHIP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
34    StorageSlotName::new("miden::standards::access::rbac::role_membership")
35        .expect("storage slot name should be valid")
36});
37
38// ROLE CONFIG
39// ================================================================================================
40
41/// A role configuration for the [`RoleBasedAccessControl`] component: the accounts holding the
42/// role and the role administering it.
43///
44/// A config establishes the state that the `grant_role` and `set_role_admin` procedures would
45/// otherwise have to reach on-chain, so an account can be created with its final role graph
46/// already in place. A config is validated only once it is passed to the
47/// [`RoleBasedAccessControl` builder][RoleBasedAccessControl::builder], which checks it against
48/// the other roles; a `RoleConfig` on its own carries no guarantee of being usable.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct RoleConfig {
51    role: RoleSymbol,
52    members: BTreeSet<AccountId>,
53    admin: Option<RoleSymbol>,
54}
55
56impl RoleConfig {
57    /// Returns an empty configuration for a new role.
58    pub fn new(role: RoleSymbol) -> Self {
59        Self {
60            role,
61            members: BTreeSet::new(),
62            admin: None,
63        }
64    }
65
66    /// Defines an admin for the role. `admin` delegates the role's administration to another role.
67    /// Leaving it unset leaves the role administered by the built-in
68    /// [`ADMIN`][RoleBasedAccessControl::ADMIN_ROLE] role.
69    ///
70    /// A role with a delegated admin but no members configures administration for a role that does
71    /// not exist yet; the role starts existing once it is granted its first member.
72    pub fn with_admin(mut self, admin: RoleSymbol) -> Self {
73        self.admin = Some(admin);
74        self
75    }
76
77    /// Adds the specified accounts to the role's member set.
78    pub fn with_members(mut self, members: impl IntoIterator<Item = AccountId>) -> Self {
79        self.members.extend(members);
80        self
81    }
82
83    /// Adds a single account to this role's member set.
84    pub fn with_member(mut self, member: AccountId) -> Self {
85        self.members.insert(member);
86        self
87    }
88}
89
90impl RoleConfig {
91    /// Returns the symbol of the role.
92    pub fn role(&self) -> &RoleSymbol {
93        &self.role
94    }
95
96    /// Returns the members set of the role.
97    pub fn members(&self) -> &BTreeSet<AccountId> {
98        &self.members
99    }
100
101    /// Returns the role administering the this role, or `None` if it is administered by the
102    /// built-in [`ADMIN`][RoleBasedAccessControl::ADMIN_ROLE] role.
103    pub fn admin(&self) -> Option<&RoleSymbol> {
104        self.admin.as_ref()
105    }
106}
107
108// ROLE BASED ACCESS CONTROL
109// ================================================================================================
110
111/// Role-based access control (RBAC) for account components.
112///
113/// Instead of having one account holding every privilege, privileges are split into named
114/// roles (for example `MINTER`, `BURNER`, `PAUSER`), and each procedure is guarded against
115/// the caller's role membership. It allows role assignment with domain isolation to minimize
116/// the scope of damage from a compromised role.
117///
118/// ## Security considerations
119///
120/// Access control is based on the note sender (the account ID that created the note), which
121/// authenticates *which account* created a note but not the *code* that executed when it was
122/// created. It is meaningful only when every account registered as a role member enforces
123/// strong authentication. Registering a permissionless account (for example one using `no_auth`)
124/// as a role member provides no access restriction: anyone can make such an account emit a
125/// note with an arbitrary script root and that account's ID as sender, defeating the sender check.
126///
127/// ## Administration model
128///
129/// Role administration is fully role-based. Every role has an *effective admin role*:
130/// its configured delegated admin when set, otherwise the built-in
131/// [`ADMIN`][Self::ADMIN_ROLE] role. Only members of a role's effective admin role may grant,
132/// revoke, or re-point (`set_role_admin`) that role.
133///
134/// A component defined any role is configured with a live administration path for it (see
135/// [`builder`][Self::builder]), which for a role left with the default admin means members of the
136/// `ADMIN` role. The `ADMIN` role administers itself, so `ADMIN` membership can be granted,
137/// revoked, and renounced through the standard API.
138///
139/// ## Role hierarchy and exclusive delegation
140///
141/// Every role may have its admin delegated to another role via `set_role_admin`. Accounts
142/// holding a role's admin role are authorized to grant and revoke that role. For example,
143/// accounts holding `MINTER_ADMIN` can manage the `MINTER` role but have no authority over
144/// `BURNER` or `PAUSER`.
145///
146/// Delegation is *exclusive* while the delegated admin role is populated: the `ADMIN` role then
147/// has no authority over the delegated role (grant, revoke, and further `set_role_admin` are
148/// gated on the delegated admin). This lets a sensitive role — say a token issuer — be placed
149/// exclusively under a dedicated admin role and kept out of reach of the general
150/// administrator. To hand authority back, the current delegated admin re-points the role
151/// (passing `0` reverts it to the `ADMIN` role).
152///
153/// Both members and delegated admins can be configured at construction (see [`RoleConfig`]), which
154/// establishes exclusive delegation atomically: a role configured with a delegated admin is never
155/// reachable by `ADMIN`, not even transiently. Reaching the same configuration on an existing
156/// account requires the sequence below, during which `ADMIN` still administers the role. That
157/// window is also the only chance to repair a mistyped or hostile admin role, so a configured
158/// delegation must be verified before account creation: initialization proves that *some* role can
159/// administer the delegated role, never that the deployer controls it.
160///
161/// This supports a fully decentralized configuration: for each delegated role, (1) grant the
162/// dedicated admin role's members, (2) make it self-administering (`set_role_admin(X, X)` —
163/// only safe once `X` has members), (3) delegate the managed role to it, and (4) revoke or
164/// renounce all bootstrap `ADMIN` members, waiting for each step to commit before issuing
165/// the next. Emptying `ADMIN` is permanent and forfeits every `ADMIN`-defaulted capability
166/// (the `Authority` procedure→role map is fixed at account creation), so an account whose
167/// gated procedures are not all mapped to live roles must never empty `ADMIN`. A
168/// self-administering role has no quorum — any single member can evict the rest — so its
169/// members should themselves be strongly authenticated (e.g. multisig) accounts.
170///
171/// The delegated admin of a role can itself be any role, including one that it admins.
172/// Circular relationships are possible but should be designed with care, since each role
173/// can then revoke the other.
174///
175/// ## Role semantics
176///
177/// A role is considered to exist when it has at least one member. Granting the first
178/// member creates the role; revoking the last member removes it. As a consequence,
179/// `set_role_admin(A, B)` stores the admin relationship in storage but does not make role
180/// `A` exist until a member is granted. Once the last member of `A` is revoked,
181/// `get_role_member_count(A)` returns `0`, though the admin configuration is retained and
182/// will apply the next time a member is granted.
183///
184/// ## Membership lookup
185///
186/// `has_role` procedure is the primary guard used by procedures that assert the caller's
187/// role membership. `get_role_member_count` returns the number of accounts holding a role.
188///
189/// ## Role symbol format
190///
191/// A [`RoleSymbol`] encodes up to 12 uppercase ASCII characters with underscores into a
192/// single field element using the same packing as the token symbol type. Examples:
193/// `MINTER`, `MINTER_ADMIN`, `PAUSER`. The zero field element is reserved and cannot be
194/// used as a role symbol; attempting to do so panics with `ERR_ROLE_SYMBOL_ZERO`.
195///
196/// On-chain a role is only ever the encoded field element, so the entrypoints that write one to
197/// storage hold it to this same encoding and panic with `ERR_INVALID_ROLE_SYMBOL` otherwise. The
198/// read guard `assert_sender_has_role` does not re-check: a non-canonical symbol matches no stored
199/// membership and fails the guard anyway. The one role symbol this component does not write is the
200/// one `Authority` keeps in its procedure-roles map, and `authority::assert_authorized_rbac`
201/// validates that on read, which keeps on-chain authorization and off-chain readers (such as
202/// [`Authority::try_from_storage`][crate::account::access::Authority::try_from_storage]) in
203/// agreement.
204///
205/// ## Usage
206///
207/// Guarding a procedure in MASM so that only members of `MINTER` can call it:
208///
209/// ```text
210/// pub proc mint
211///     push.MINTER_ROLE_SYMBOL
212///     exec.::miden::standards::access::rbac::assert_sender_has_role
213///     # add mint logic
214/// end
215/// ```
216///
217/// [`RoleSymbol`]: miden_protocol::account::RoleSymbol
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct RoleBasedAccessControl {
220    /// The roles defined at construction, keyed by their symbol. May be empty, in which case the
221    /// component starts with no administrator and no role members.
222    roles: BTreeMap<RoleSymbol, RoleConfig>,
223}
224
225#[bon::bon]
226impl RoleBasedAccessControl {
227    /// Returns an RBAC component initialized with the given roles, each carrying its members and
228    /// its delegated admin (see [`RoleConfig`]).
229    ///
230    /// Roles are added with the [`role`][RoleBasedAccessControlBuilder::role] and
231    /// [`roles`][RoleBasedAccessControlBuilder::roles] setters. Initializing no role at all is
232    /// allowed and produces a component with no roles and no administrator.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if:
237    /// - the same role is specified more than once.
238    /// - a role is configured with neither members nor a delegated admin.
239    /// - a role's member count exceeds [`u32::MAX`].
240    /// - `ADMIN` is defined without members, or not defined at all, and a role's admin chain never
241    ///   reaches a populated role, which would leave that role permanently unmanageable. A
242    ///   populated `ADMIN` administers every role whose delegated admin is memberless, so it makes
243    ///   any admin chain recoverable; without one, defining an operational role and no `ADMIN` is
244    ///   the common defect, since `ADMIN` administers itself and nothing can ever populate it.
245    #[builder]
246    pub fn new(
247        #[builder(field)] role_configs: Vec<RoleConfig>,
248    ) -> Result<Self, RoleBasedAccessControlError> {
249        let mut roles = BTreeMap::new();
250        for config in role_configs {
251            if config.members.is_empty() && config.admin.is_none() {
252                return Err(RoleBasedAccessControlError::EmptyRoleConfig(config.role));
253            }
254            if u32::try_from(config.members.len()).is_err() {
255                return Err(RoleBasedAccessControlError::MemberCountOverflow {
256                    role: config.role,
257                    member_count: config.members.len(),
258                });
259            }
260            if roles.contains_key(&config.role) {
261                return Err(RoleBasedAccessControlError::DuplicateRole(config.role));
262            }
263            roles.insert(config.role.clone(), config);
264        }
265
266        // Prevent creating unmanagable role graphs by checking that roles are administered,
267        // directly or indirectly, by an admin role that has members, otherwise the role could not
268        // be actively managed. Delegated admin roles that are memberless fall back to the authority
269        // of the built-in admin role. So, there are two cases:
270        // - If the default admin role is populated, this is globally ensured due to the fallback.
271        // - If the default admin role is not populated, check that every role is administered by a
272        //   role that has members (e.g. its direct admin, or indirectly through the admin's admin,
273        //   and so on).
274        //
275        // Note that this does not prevent decentralized setups which don't make use of the built-in
276        // admin role, so long as the terminating admin role administers itself. For example, a
277        // Pauser + PauserAdmin and a Burner + BurnerAdmin can set up completely independently of
278        // one another and without the built-in admin, if PauserAdmin and BurnerAdmin administer
279        // themselves.
280        let admin_is_populated =
281            roles.get(&Self::admin_role()).is_some_and(|config| !config.members.is_empty());
282        if !admin_is_populated {
283            for role_config in roles.values() {
284                let admin = role_config.admin.clone().unwrap_or_else(Self::admin_role);
285                if !reaches_populated_role(&admin, &roles) {
286                    return Err(RoleBasedAccessControlError::UnmanageableRole {
287                        role: role_config.role.clone(),
288                        admin,
289                    });
290                }
291            }
292        }
293
294        Ok(Self { roles })
295    }
296}
297
298impl RoleBasedAccessControl {
299    /// The name of the component.
300    pub const NAME: &'static str = "miden::standards::access::rbac";
301
302    /// The built-in default admin role symbol. A role whose delegated admin is unset is
303    /// administered by members of this role.
304    ///
305    /// Keep in sync with the `ADMIN_ROLE` constant in `asm/standards/access/rbac.masm`.
306    pub const ADMIN_ROLE: &'static str = "ADMIN";
307
308    // CONSTRUCTORS
309    // --------------------------------------------------------------------------------------------
310
311    /// Returns an RBAC component whose built-in [`ADMIN`][Self::ADMIN_ROLE] role is configured
312    /// with `admins` and which defines no other role.
313    ///
314    /// # Errors
315    ///
316    /// Returns an error if `admins` is empty, since the resulting component would have no
317    /// administrator. Build such a component with the [`builder`][Self::builder] instead.
318    pub fn with_admins(
319        admins: impl IntoIterator<Item = AccountId>,
320    ) -> Result<Self, RoleBasedAccessControlError> {
321        Self::builder()
322            .role(RoleConfig::new(Self::admin_role()).with_members(admins))
323            .build()
324    }
325
326    // PUBLIC ACCESSORS
327    // --------------------------------------------------------------------------------------------
328
329    /// Returns the built-in default admin [`RoleSymbol`].
330    pub fn admin_role() -> RoleSymbol {
331        RoleSymbol::new(Self::ADMIN_ROLE).expect("ADMIN is a valid role symbol")
332    }
333
334    /// Returns the canonical [`AccountComponentName`] of this component.
335    pub const fn name() -> AccountComponentName {
336        AccountComponentName::from_static_str(Self::NAME)
337    }
338
339    /// Returns the [`AccountComponentCode`] of this component.
340    pub fn code() -> &'static AccountComponentCode {
341        &RBAC_CODE
342    }
343
344    /// Returns the storage slot name for the per-role config map.
345    pub fn role_config_slot() -> &'static StorageSlotName {
346        &ROLE_CONFIG_SLOT_NAME
347    }
348
349    /// Returns the storage slot name for the per-role membership map.
350    pub fn role_membership_slot() -> &'static StorageSlotName {
351        &ROLE_MEMBERSHIP_SLOT_NAME
352    }
353
354    /// Returns the schema entry for the per-role config map.
355    pub fn role_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
356        (
357            Self::role_config_slot().clone(),
358            StorageSlotSchema::map(
359                "Per-role RBAC configuration (member count and delegated admin role)",
360                SchemaType::role_symbol(),
361                SchemaType::native_word(),
362            ),
363        )
364    }
365
366    /// Returns the schema entry for the per-role membership map.
367    pub fn role_membership_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
368        (
369            Self::role_membership_slot().clone(),
370            StorageSlotSchema::map(
371                "Role membership flag indexed by role symbol and account ID",
372                SchemaType::native_word(),
373                SchemaType::native_word(),
374            ),
375        )
376    }
377
378    /// Returns the [`AccountComponentMetadata`] describing this component.
379    pub fn component_metadata() -> AccountComponentMetadata {
380        let storage_schema = StorageSchema::new(vec![
381            Self::role_config_slot_schema(),
382            Self::role_membership_slot_schema(),
383        ])
384        .expect("storage schema should be valid");
385
386        AccountComponentMetadata::new(Self::NAME)
387            .with_description("Role-based access control component")
388            .with_storage_schema(storage_schema)
389    }
390}
391
392impl<S: role_based_access_control_builder::State> RoleBasedAccessControlBuilder<S> {
393    /// Adds a single role to the component.
394    pub fn role(mut self, config: RoleConfig) -> Self {
395        self.role_configs.push(config);
396        self
397    }
398
399    /// Adds multiple role to the component.
400    pub fn roles(mut self, configs: impl IntoIterator<Item = RoleConfig>) -> Self {
401        self.role_configs.extend(configs);
402        self
403    }
404}
405
406// HELPERS
407// ================================================================================================
408
409/// Returns `true` if walking the delegated-admin chain starting at `role` reaches a role defined
410/// with at least one member.
411///
412/// A role that is not configured, or configured without members, is administered by its delegated
413/// admin, defaulting to `ADMIN`. Every role has exactly one admin, so the walk always ends in a
414/// cycle, which the visited set terminates.
415fn reaches_populated_role(role: &RoleSymbol, configs: &BTreeMap<RoleSymbol, RoleConfig>) -> bool {
416    let admin_role = RoleBasedAccessControl::admin_role();
417    let mut visited = BTreeSet::new();
418    let mut current = role.clone();
419
420    while visited.insert(current.clone()) {
421        current = match configs.get(&current) {
422            Some(role) if !role.members.is_empty() => return true,
423            Some(role) => role.admin.clone().unwrap_or_else(|| admin_role.clone()),
424            None => admin_role.clone(),
425        };
426    }
427
428    false
429}
430
431// CONVERSIONS
432// ================================================================================================
433
434impl From<RoleBasedAccessControl> for AccountComponent {
435    fn from(rbac: RoleBasedAccessControl) -> Self {
436        // Config, for every role:
437        // - role_config:     [0, 0, 0, role] -> [member_count, admin_role, 0, 0]
438        // - role_membership: [0, role, acct_suffix, acct_prefix] -> [1, 0, 0, 0]
439        let mut config_entries = Vec::new();
440        let mut membership_entries = Vec::new();
441        for config in rbac.roles.into_values() {
442            let role_symbol: Felt = config.role.as_element();
443            let member_count = u32::try_from(config.members.len())
444                .expect("member count is validated on initialization");
445            let admin_symbol = config.admin.as_ref().map_or(Felt::ZERO, RoleSymbol::as_element);
446            config_entries.push((
447                StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, role_symbol])),
448                Word::from([Felt::from(member_count), admin_symbol, Felt::ZERO, Felt::ZERO]),
449            ));
450            for member in config.members {
451                membership_entries.push((
452                    StorageMapKey::new(Word::from([
453                        Felt::ZERO,
454                        role_symbol,
455                        member.suffix(),
456                        member.prefix().as_felt(),
457                    ])),
458                    Word::from([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]),
459                ));
460            }
461        }
462
463        let role_membership_map = StorageMap::with_entries(membership_entries)
464            .expect("config role membership map should be valid");
465        let role_config_map = StorageMap::with_entries(config_entries)
466            .expect("config role config map should be valid");
467
468        let role_config_slot = StorageSlot::with_map(
469            RoleBasedAccessControl::role_config_slot().clone(),
470            role_config_map,
471        );
472        let role_membership_slot = StorageSlot::with_map(
473            RoleBasedAccessControl::role_membership_slot().clone(),
474            role_membership_map,
475        );
476
477        AccountComponent::new(
478            RoleBasedAccessControl::code().clone(),
479            vec![role_config_slot, role_membership_slot],
480            RoleBasedAccessControl::component_metadata(),
481        )
482        .expect("RBAC component should satisfy the requirements of a valid account component")
483    }
484}
485
486// ROLE BASED ACCESS CONTROL ERROR
487// ================================================================================================
488
489/// Errors that can occur when initializing the [`RoleBasedAccessControl`] component.
490#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
491pub enum RoleBasedAccessControlError {
492    #[error("role {0} is defined more than once")]
493    DuplicateRole(RoleSymbol),
494    #[error("role {0} is defined with neither members nor a delegated admin")]
495    EmptyRoleConfig(RoleSymbol),
496    #[error(
497        "role {role} is defined with {member_count} members which exceeds the maximum of {}",
498        u32::MAX
499    )]
500    MemberCountOverflow { role: RoleSymbol, member_count: usize },
501    #[error(
502        "role {role} is defined with delegated admin {admin}, which can never hold members and so leaves {role} unmanageable"
503    )]
504    UnmanageableRole { role: RoleSymbol, admin: RoleSymbol },
505}
506
507// TESTS
508// ================================================================================================
509
510#[cfg(test)]
511mod tests {
512    use miden_protocol::account::{AccountType, StorageSlotContent};
513
514    use super::*;
515
516    fn test_admin(seed: u8) -> AccountId {
517        AccountId::builder()
518            .account_type(AccountType::Private)
519            .build_with_seed([seed; 32])
520    }
521
522    fn role(symbol: &str) -> RoleSymbol {
523        RoleSymbol::new_unchecked(symbol)
524    }
525
526    /// Returns the role config map key of the given role.
527    fn role_config_key(role: &RoleSymbol) -> StorageMapKey {
528        StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, role.as_element()]))
529    }
530
531    /// Returns the map content of the component's storage slot with the given name.
532    fn find_map<'a>(
533        component: &'a AccountComponent,
534        slot_name: &StorageSlotName,
535    ) -> &'a StorageMap {
536        let slot = component
537            .storage_slots()
538            .iter()
539            .find(|slot| slot.name() == slot_name)
540            .expect("component should register the slot");
541        match slot.content() {
542            StorageSlotContent::Map(map) => map,
543            _ => panic!("slot {slot_name} should be a map"),
544        }
545    }
546
547    #[test]
548    fn admin_role_encoding_matches_masm_constant() {
549        // Must stay in sync with `const ADMIN_ROLE` in asm/standards/access/rbac.masm.
550        const MASM_ADMIN_ROLE: u64 = 1836707;
551        assert_eq!(
552            RoleBasedAccessControl::admin_role().as_element().as_canonical_u64(),
553            MASM_ADMIN_ROLE,
554        );
555    }
556
557    #[test]
558    fn with_admins_sets_every_admin_and_the_member_count() -> anyhow::Result<()> {
559        // Members are held in a `BTreeSet`, so duplicate account IDs collapse before this point
560        // and the member count always matches the number of membership entries.
561        let admins = [test_admin(1), test_admin(2), test_admin(3)];
562        let component: AccountComponent = RoleBasedAccessControl::with_admins(admins)?.into();
563
564        let admin_symbol = RoleBasedAccessControl::admin_role().as_element();
565
566        let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
567        assert_eq!(membership.num_entries(), admins.len());
568        for admin in admins {
569            let key = StorageMapKey::new(Word::from([
570                Felt::ZERO,
571                admin_symbol,
572                admin.suffix(),
573                admin.prefix().as_felt(),
574            ]));
575            assert_eq!(
576                membership.get(&key),
577                Word::from([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO])
578            );
579        }
580
581        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
582        let member_count = u32::try_from(admins.len())?;
583        assert_eq!(
584            config.get(&role_config_key(&RoleBasedAccessControl::admin_role())),
585            Word::from([Felt::from(member_count), Felt::ZERO, Felt::ZERO, Felt::ZERO]),
586        );
587
588        Ok(())
589    }
590
591    #[test]
592    fn with_admins_rejects_an_empty_member_set() {
593        let error =
594            RoleBasedAccessControl::with_admins([]).expect_err("initialization should have failed");
595
596        assert_eq!(
597            error,
598            RoleBasedAccessControlError::EmptyRoleConfig(RoleBasedAccessControl::admin_role())
599        );
600    }
601
602    #[test]
603    fn defining_no_role_defines_no_admin() -> anyhow::Result<()> {
604        let component: AccountComponent = RoleBasedAccessControl::builder().build()?.into();
605
606        // No membership entries and an empty config: the component starts with no administrator.
607        let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
608        assert_eq!(membership.num_entries(), 0);
609        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
610        assert_eq!(config.num_entries(), 0);
611
612        Ok(())
613    }
614
615    /// A delegated admin is defined in the role config, which places the role out of `ADMIN`'s
616    /// reach without any on-chain `set_role_admin`.
617    #[test]
618    fn defining_delegated_admin_is_written_to_the_role_config() -> anyhow::Result<()> {
619        let admin = test_admin(1);
620        let manager = test_admin(2);
621        let pauser = test_admin(3);
622
623        let manager_role = RoleSymbol::new("DOM_MANAGER")?;
624        let pauser_role = RoleSymbol::new("DOM_PAUSER")?;
625
626        let component: AccountComponent = RoleBasedAccessControl::builder()
627            .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(admin))
628            // DOM_MANAGER administers itself, so ADMIN cannot rotate its membership.
629            .role(
630                RoleConfig::new(manager_role.clone())
631                    .with_member(manager)
632                    .with_admin(manager_role.clone()),
633            )
634            .role(
635                RoleConfig::new(pauser_role.clone())
636                    .with_member(pauser)
637                    .with_admin(manager_role.clone()),
638            )
639            .build()?
640            .into();
641
642        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
643        let manager_symbol = manager_role.as_element();
644        assert_eq!(
645            config.get(&role_config_key(&manager_role)),
646            Word::from([Felt::ONE, manager_symbol, Felt::ZERO, Felt::ZERO]),
647        );
648        assert_eq!(
649            config.get(&role_config_key(&pauser_role)),
650            Word::from([Felt::ONE, manager_symbol, Felt::ZERO, Felt::ZERO]),
651        );
652
653        Ok(())
654    }
655
656    /// Delegating the admin of a role that has no members yet is what `set_role_admin` does on an
657    /// existing account, so initializing it must be expressible too.
658    #[test]
659    fn role_initialized_without_members_holds_its_delegated_admin() -> anyhow::Result<()> {
660        let admin = test_admin(1);
661        let minter_role = RoleSymbol::new("MINTER")?;
662        let minter_admin_role = RoleBasedAccessControl::admin_role();
663
664        let component: AccountComponent = RoleBasedAccessControl::builder()
665            .role(RoleConfig::new(minter_admin_role.clone()).with_member(admin))
666            .role(RoleConfig::new(minter_role.clone()).with_admin(minter_admin_role))
667            .build()?
668            .into();
669
670        // The role has a config entry but no members, so it does not exist yet.
671        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
672        assert_eq!(
673            config.get(&role_config_key(&minter_role)),
674            Word::from([
675                Felt::ZERO,
676                RoleBasedAccessControl::admin_role().as_element(),
677                Felt::ZERO,
678                Felt::ZERO
679            ]),
680        );
681        let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
682        assert_eq!(membership.num_entries(), 1);
683
684        Ok(())
685    }
686
687    /// A populated `ADMIN` administers every role whose delegated admin is memberless, so even an
688    /// admin chain that reaches no populated role of its own leaves the role manageable.
689    #[test]
690    fn populated_admin_allows_an_otherwise_unmanageable_delegation() -> anyhow::Result<()> {
691        let admin = test_admin(1);
692        let minter_role = RoleSymbol::new("MINTER")?;
693        let minter_admin_role = RoleSymbol::new("MINTER_ADMIN")?;
694
695        // MINTER_ADMIN administers itself and has no members, so nothing in MINTER's chain can be
696        // populated by the chain itself — ADMIN takes over administering both.
697        RoleBasedAccessControl::builder()
698            .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(admin))
699            .role(RoleConfig::new(minter_role).with_admin(minter_admin_role.clone()))
700            .role(RoleConfig::new(minter_admin_role.clone()).with_admin(minter_admin_role))
701            .build()?;
702
703        Ok(())
704    }
705
706    /// A role whose delegated admin is empty is still manageable as long as the admin itself can
707    /// be populated, which is the case while `ADMIN` is populated.
708    #[test]
709    fn delegating_to_a_role_populated_later_is_allowed() -> anyhow::Result<()> {
710        let admin = test_admin(1);
711        let minter_role = RoleSymbol::new("MINTER")?;
712        let minter_admin_role = RoleSymbol::new("MINTER_ADMIN")?;
713
714        RoleBasedAccessControl::builder()
715            .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(admin))
716            .role(RoleConfig::new(minter_role).with_admin(minter_admin_role))
717            .build()?;
718
719        Ok(())
720    }
721
722    #[rstest::rstest]
723    #[case::duplicate_role(
724        vec![
725            RoleConfig::new(role("MINTER")).with_member(test_admin(1)),
726            RoleConfig::new(role("MINTER")).with_member(test_admin(2)),
727        ],
728        RoleBasedAccessControlError::DuplicateRole(role("MINTER")),
729    )]
730    #[case::empty_config(
731        vec![RoleConfig::new(role("MINTER"))],
732        RoleBasedAccessControlError::EmptyRoleConfig(role("MINTER")),
733    )]
734    // MINTER delegates to a self-administering role that has no members, so nobody can ever
735    // populate MINTER_ADMIN and MINTER stays unmanageable.
736    #[case::unmanageable_role(
737        vec![
738            RoleConfig::new(role("MINTER")).with_admin(role("MINTER_ADMIN")),
739            RoleConfig::new(role("MINTER_ADMIN")).with_admin(role("MINTER_ADMIN")),
740        ],
741        RoleBasedAccessControlError::UnmanageableRole {
742            role: role("MINTER"),
743            admin: role("MINTER_ADMIN"),
744        },
745    )]
746    #[case::empty_admins(
747        vec![RoleConfig::new(RoleBasedAccessControl::admin_role())],
748        RoleBasedAccessControlError::EmptyRoleConfig(RoleBasedAccessControl::admin_role()),
749    )]
750    // Leaving MINTER's admin unset makes ADMIN administer it, but ADMIN administers itself, so an
751    // unspecified ADMIN can never hold members. This is the same defect as `unmanageable_role`,
752    // spelled implicitly.
753    #[case::unspecified_default_admin(
754        vec![RoleConfig::new(role("MINTER")).with_member(test_admin(1))],
755        RoleBasedAccessControlError::UnmanageableRole {
756            role: role("MINTER"),
757            admin: RoleBasedAccessControl::admin_role(),
758        },
759    )]
760    fn invalid_role_configs_are_rejected(
761        #[case] configs: Vec<RoleConfig>,
762        #[case] expected: RoleBasedAccessControlError,
763    ) {
764        let error = RoleBasedAccessControl::builder()
765            .roles(configs)
766            .build()
767            .expect_err("initialization should have failed");
768
769        assert_eq!(error, expected);
770    }
771}