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*: once a role's admin is delegated to another role, the `ADMIN`
147/// role loses all authority over it (grant, revoke, and further `set_role_admin` are then
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. Only delegate to a role that already has members, and treat
174/// emptying a role's effective admin like ownership renouncement: the role stays
175/// unmanageable until its effective admin is repopulated — for a self-administering role
176/// (including `ADMIN`), never.
177///
178/// ## Role semantics
179///
180/// A role is considered to exist when it has at least one member. Granting the first
181/// member creates the role; revoking the last member removes it. As a consequence,
182/// `set_role_admin(A, B)` stores the admin relationship in storage but does not make role
183/// `A` exist until a member is granted. Once the last member of `A` is revoked,
184/// `get_role_member_count(A)` returns `0`, though the admin configuration is retained and
185/// will apply the next time a member is granted.
186///
187/// ## Membership lookup
188///
189/// `has_role` procedure is the primary guard used by procedures that assert the caller's
190/// role membership. `get_role_member_count` returns the number of accounts holding a role.
191///
192/// ## Role symbol format
193///
194/// A [`RoleSymbol`] encodes up to 12 uppercase ASCII characters with underscores into a
195/// single field element using the same packing as the token symbol type. Examples:
196/// `MINTER`, `MINTER_ADMIN`, `PAUSER`. The zero field element is reserved and cannot be
197/// used as a role symbol; attempting to do so panics with `ERR_ROLE_SYMBOL_ZERO`.
198///
199/// ## Usage
200///
201/// Guarding a procedure in MASM so that only members of `MINTER` can call it:
202///
203/// ```text
204/// pub proc mint
205///     push.MINTER_ROLE_SYMBOL
206///     exec.::miden::standards::access::rbac::assert_sender_has_role
207///     # add mint logic
208/// end
209/// ```
210///
211/// [`RoleSymbol`]: miden_protocol::account::RoleSymbol
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct RoleBasedAccessControl {
214    /// The roles defined at construction, keyed by their symbol. May be empty, in which case the
215    /// component starts with no administrator and no role members.
216    roles: BTreeMap<RoleSymbol, RoleConfig>,
217}
218
219#[bon::bon]
220impl RoleBasedAccessControl {
221    /// Returns an RBAC component initialized with the given roles, each carrying its members and
222    /// its delegated admin (see [`RoleConfig`]).
223    ///
224    /// Roles are added with the [`role`][RoleBasedAccessControlBuilder::role] and
225    /// [`roles`][RoleBasedAccessControlBuilder::roles] setters. Initializing no role at all is
226    /// allowed and produces a component with no roles and no administrator.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if:
231    /// - the same role is specified more than once.
232    /// - a role is configured with neither members nor a delegated admin.
233    /// - a role's member count exceeds [`u32::MAX`].
234    /// - a role's effective admin — its delegated admin, or `ADMIN` when unset — can never hold
235    ///   members, which would leave the role permanently unmanageable. Setting an operational role
236    ///   without defining `ADMIN` is the common case: `ADMIN` administers itself, so nothing can
237    ///   ever populate it.
238    #[builder]
239    pub fn new(
240        #[builder(field)] role_configs: Vec<RoleConfig>,
241    ) -> Result<Self, RoleBasedAccessControlError> {
242        let mut roles = BTreeMap::new();
243        for config in role_configs {
244            if config.members.is_empty() && config.admin.is_none() {
245                return Err(RoleBasedAccessControlError::EmptyRoleConfig(config.role));
246            }
247            if u32::try_from(config.members.len()).is_err() {
248                return Err(RoleBasedAccessControlError::MemberCountOverflow {
249                    role: config.role,
250                    member_count: config.members.len(),
251                });
252            }
253            if roles.contains_key(&config.role) {
254                return Err(RoleBasedAccessControlError::DuplicateRole(config.role));
255            }
256            roles.insert(config.role.clone(), config);
257        }
258
259        // Check the effective admin of every role, not just of the explicitly delegated ones: a
260        // role left with the default admin is just as frozen when `ADMIN` can never hold members.
261        for role_config in roles.values() {
262            let admin = role_config.admin.clone().unwrap_or_else(Self::admin_role);
263            if !reaches_populated_role(&admin, &roles) {
264                return Err(RoleBasedAccessControlError::UnmanageableRole {
265                    role: role_config.role.clone(),
266                    admin,
267                });
268            }
269        }
270
271        Ok(Self { roles })
272    }
273}
274
275impl RoleBasedAccessControl {
276    /// The name of the component.
277    pub const NAME: &'static str = "miden::standards::access::rbac";
278
279    /// The built-in default admin role symbol. A role whose delegated admin is unset is
280    /// administered by members of this role.
281    ///
282    /// Keep in sync with the `ADMIN_ROLE` constant in `asm/standards/access/rbac.masm`.
283    pub const ADMIN_ROLE: &'static str = "ADMIN";
284
285    // CONSTRUCTORS
286    // --------------------------------------------------------------------------------------------
287
288    /// Returns an RBAC component whose built-in [`ADMIN`][Self::ADMIN_ROLE] role is configured
289    /// with `admins` and which defines no other role.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if `admins` is empty, since the resulting component would have no
294    /// administrator. Build such a component with the [`builder`][Self::builder] instead.
295    pub fn with_admins(
296        admins: impl IntoIterator<Item = AccountId>,
297    ) -> Result<Self, RoleBasedAccessControlError> {
298        Self::builder()
299            .role(RoleConfig::new(Self::admin_role()).with_members(admins))
300            .build()
301    }
302
303    // PUBLIC ACCESSORS
304    // --------------------------------------------------------------------------------------------
305
306    /// Returns the built-in default admin [`RoleSymbol`].
307    pub fn admin_role() -> RoleSymbol {
308        RoleSymbol::new(Self::ADMIN_ROLE).expect("ADMIN is a valid role symbol")
309    }
310
311    /// Returns the canonical [`AccountComponentName`] of this component.
312    pub const fn name() -> AccountComponentName {
313        AccountComponentName::from_static_str(Self::NAME)
314    }
315
316    /// Returns the [`AccountComponentCode`] of this component.
317    pub fn code() -> &'static AccountComponentCode {
318        &RBAC_CODE
319    }
320
321    /// Returns the storage slot name for the per-role config map.
322    pub fn role_config_slot() -> &'static StorageSlotName {
323        &ROLE_CONFIG_SLOT_NAME
324    }
325
326    /// Returns the storage slot name for the per-role membership map.
327    pub fn role_membership_slot() -> &'static StorageSlotName {
328        &ROLE_MEMBERSHIP_SLOT_NAME
329    }
330
331    /// Returns the schema entry for the per-role config map.
332    pub fn role_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
333        (
334            Self::role_config_slot().clone(),
335            StorageSlotSchema::map(
336                "Per-role RBAC configuration (member count and delegated admin role)",
337                SchemaType::role_symbol(),
338                SchemaType::native_word(),
339            ),
340        )
341    }
342
343    /// Returns the schema entry for the per-role membership map.
344    pub fn role_membership_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
345        (
346            Self::role_membership_slot().clone(),
347            StorageSlotSchema::map(
348                "Role membership flag indexed by role symbol and account ID",
349                SchemaType::native_word(),
350                SchemaType::native_word(),
351            ),
352        )
353    }
354
355    /// Returns the [`AccountComponentMetadata`] describing this component.
356    pub fn component_metadata() -> AccountComponentMetadata {
357        let storage_schema = StorageSchema::new(vec![
358            Self::role_config_slot_schema(),
359            Self::role_membership_slot_schema(),
360        ])
361        .expect("storage schema should be valid");
362
363        AccountComponentMetadata::new(Self::NAME)
364            .with_description("Role-based access control component")
365            .with_storage_schema(storage_schema)
366    }
367}
368
369impl<S: role_based_access_control_builder::State> RoleBasedAccessControlBuilder<S> {
370    /// Adds a single role to the component.
371    pub fn role(mut self, config: RoleConfig) -> Self {
372        self.role_configs.push(config);
373        self
374    }
375
376    /// Adds multiple role to the component.
377    pub fn roles(mut self, configs: impl IntoIterator<Item = RoleConfig>) -> Self {
378        self.role_configs.extend(configs);
379        self
380    }
381}
382
383// HELPERS
384// ================================================================================================
385
386/// Returns `true` if walking the delegated-admin chain starting at `role` reaches a role defined
387/// with at least one member.
388///
389/// Only a populated role can grant members to the role below it in the chain, so a chain that
390/// reaches none of them can never be acted on by anyone. A role that is not configured, or
391/// configured without members, is administered by its delegated admin, defaulting to `ADMIN`.
392/// Every role has exactly one admin, so the walk always ends in a cycle, which the visited set
393/// terminates.
394fn reaches_populated_role(role: &RoleSymbol, configs: &BTreeMap<RoleSymbol, RoleConfig>) -> bool {
395    let admin_role = RoleBasedAccessControl::admin_role();
396    let mut visited = BTreeSet::new();
397    let mut current = role.clone();
398
399    while visited.insert(current.clone()) {
400        current = match configs.get(&current) {
401            Some(role) if !role.members.is_empty() => return true,
402            Some(role) => role.admin.clone().unwrap_or_else(|| admin_role.clone()),
403            None => admin_role.clone(),
404        };
405    }
406
407    false
408}
409
410// CONVERSIONS
411// ================================================================================================
412
413impl From<RoleBasedAccessControl> for AccountComponent {
414    fn from(rbac: RoleBasedAccessControl) -> Self {
415        // Config, for every role:
416        // - role_config:     [0, 0, 0, role] -> [member_count, admin_role, 0, 0]
417        // - role_membership: [0, role, acct_suffix, acct_prefix] -> [1, 0, 0, 0]
418        let mut config_entries = Vec::new();
419        let mut membership_entries = Vec::new();
420        for config in rbac.roles.into_values() {
421            let role_symbol: Felt = config.role.as_element();
422            let member_count = u32::try_from(config.members.len())
423                .expect("member count is validated on initialization");
424            let admin_symbol = config.admin.as_ref().map_or(Felt::ZERO, RoleSymbol::as_element);
425            config_entries.push((
426                StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, role_symbol])),
427                Word::from([Felt::from(member_count), admin_symbol, Felt::ZERO, Felt::ZERO]),
428            ));
429            for member in config.members {
430                membership_entries.push((
431                    StorageMapKey::new(Word::from([
432                        Felt::ZERO,
433                        role_symbol,
434                        member.suffix(),
435                        member.prefix().as_felt(),
436                    ])),
437                    Word::from([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]),
438                ));
439            }
440        }
441
442        let role_membership_map = StorageMap::with_entries(membership_entries)
443            .expect("config role membership map should be valid");
444        let role_config_map = StorageMap::with_entries(config_entries)
445            .expect("config role config map should be valid");
446
447        let role_config_slot = StorageSlot::with_map(
448            RoleBasedAccessControl::role_config_slot().clone(),
449            role_config_map,
450        );
451        let role_membership_slot = StorageSlot::with_map(
452            RoleBasedAccessControl::role_membership_slot().clone(),
453            role_membership_map,
454        );
455
456        AccountComponent::new(
457            RoleBasedAccessControl::code().clone(),
458            vec![role_config_slot, role_membership_slot],
459            RoleBasedAccessControl::component_metadata(),
460        )
461        .expect("RBAC component should satisfy the requirements of a valid account component")
462    }
463}
464
465// ROLE BASED ACCESS CONTROL ERROR
466// ================================================================================================
467
468/// Errors that can occur when initializing the [`RoleBasedAccessControl`] component.
469#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
470pub enum RoleBasedAccessControlError {
471    #[error("role {0} is defined more than once")]
472    DuplicateRole(RoleSymbol),
473    #[error("role {0} is defined with neither members nor a delegated admin")]
474    EmptyRoleConfig(RoleSymbol),
475    #[error(
476        "role {role} is defined with {member_count} members which exceeds the maximum of {}",
477        u32::MAX
478    )]
479    MemberCountOverflow { role: RoleSymbol, member_count: usize },
480    #[error(
481        "role {role} is defined with delegated admin {admin}, which can never hold members and so leaves {role} unmanageable"
482    )]
483    UnmanageableRole { role: RoleSymbol, admin: RoleSymbol },
484}
485
486// TESTS
487// ================================================================================================
488
489#[cfg(test)]
490mod tests {
491    use miden_protocol::account::{AccountType, StorageSlotContent};
492
493    use super::*;
494
495    fn test_admin(seed: u8) -> AccountId {
496        AccountId::builder()
497            .account_type(AccountType::Private)
498            .build_with_seed([seed; 32])
499    }
500
501    fn role(symbol: &str) -> RoleSymbol {
502        RoleSymbol::new_unchecked(symbol)
503    }
504
505    /// Returns the role config map key of the given role.
506    fn role_config_key(role: &RoleSymbol) -> StorageMapKey {
507        StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, role.as_element()]))
508    }
509
510    /// Returns the map content of the component's storage slot with the given name.
511    fn find_map<'a>(
512        component: &'a AccountComponent,
513        slot_name: &StorageSlotName,
514    ) -> &'a StorageMap {
515        let slot = component
516            .storage_slots()
517            .iter()
518            .find(|slot| slot.name() == slot_name)
519            .expect("component should register the slot");
520        match slot.content() {
521            StorageSlotContent::Map(map) => map,
522            _ => panic!("slot {slot_name} should be a map"),
523        }
524    }
525
526    #[test]
527    fn admin_role_encoding_matches_masm_constant() {
528        // Must stay in sync with `const ADMIN_ROLE` in asm/standards/access/rbac.masm.
529        const MASM_ADMIN_ROLE: u64 = 1836707;
530        assert_eq!(
531            RoleBasedAccessControl::admin_role().as_element().as_canonical_u64(),
532            MASM_ADMIN_ROLE,
533        );
534    }
535
536    #[test]
537    fn with_admins_sets_every_admin_and_the_member_count() -> anyhow::Result<()> {
538        // Members are held in a `BTreeSet`, so duplicate account IDs collapse before this point
539        // and the member count always matches the number of membership entries.
540        let admins = [test_admin(1), test_admin(2), test_admin(3)];
541        let component: AccountComponent = RoleBasedAccessControl::with_admins(admins)?.into();
542
543        let admin_symbol = RoleBasedAccessControl::admin_role().as_element();
544
545        let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
546        assert_eq!(membership.num_entries(), admins.len());
547        for admin in admins {
548            let key = StorageMapKey::new(Word::from([
549                Felt::ZERO,
550                admin_symbol,
551                admin.suffix(),
552                admin.prefix().as_felt(),
553            ]));
554            assert_eq!(
555                membership.get(&key),
556                Word::from([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO])
557            );
558        }
559
560        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
561        let member_count = u32::try_from(admins.len())?;
562        assert_eq!(
563            config.get(&role_config_key(&RoleBasedAccessControl::admin_role())),
564            Word::from([Felt::from(member_count), Felt::ZERO, Felt::ZERO, Felt::ZERO]),
565        );
566
567        Ok(())
568    }
569
570    #[test]
571    fn with_admins_rejects_an_empty_member_set() {
572        let error =
573            RoleBasedAccessControl::with_admins([]).expect_err("initialization should have failed");
574
575        assert_eq!(
576            error,
577            RoleBasedAccessControlError::EmptyRoleConfig(RoleBasedAccessControl::admin_role())
578        );
579    }
580
581    #[test]
582    fn defining_no_role_defines_no_admin() -> anyhow::Result<()> {
583        let component: AccountComponent = RoleBasedAccessControl::builder().build()?.into();
584
585        // No membership entries and an empty config: the component starts with no administrator.
586        let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
587        assert_eq!(membership.num_entries(), 0);
588        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
589        assert_eq!(config.num_entries(), 0);
590
591        Ok(())
592    }
593
594    /// A delegated admin is defined in the role config, which places the role out of `ADMIN`'s
595    /// reach without any on-chain `set_role_admin`.
596    #[test]
597    fn defining_delegated_admin_is_written_to_the_role_config() -> anyhow::Result<()> {
598        let admin = test_admin(1);
599        let manager = test_admin(2);
600        let pauser = test_admin(3);
601
602        let manager_role = RoleSymbol::new("DOM_MANAGER")?;
603        let pauser_role = RoleSymbol::new("DOM_PAUSER")?;
604
605        let component: AccountComponent = RoleBasedAccessControl::builder()
606            .role(
607                RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(admin),
608            )
609            // DOM_MANAGER administers itself, so ADMIN cannot rotate its membership.
610            .role(
611                RoleConfig::new(manager_role.clone()).with_member(manager)
612                    .with_admin(manager_role.clone())
613            )
614            .role(
615                RoleConfig::new(pauser_role.clone()).with_member(pauser)
616                    .with_admin(manager_role.clone())
617            )
618            .build()?
619            .into();
620
621        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
622        let manager_symbol = manager_role.as_element();
623        assert_eq!(
624            config.get(&role_config_key(&manager_role)),
625            Word::from([Felt::ONE, manager_symbol, Felt::ZERO, Felt::ZERO]),
626        );
627        assert_eq!(
628            config.get(&role_config_key(&pauser_role)),
629            Word::from([Felt::ONE, manager_symbol, Felt::ZERO, Felt::ZERO]),
630        );
631
632        Ok(())
633    }
634
635    /// Delegating the admin of a role that has no members yet is what `set_role_admin` does on an
636    /// existing account, so initializing it must be expressible too.
637    #[test]
638    fn role_initialized_without_members_holds_its_delegated_admin() -> anyhow::Result<()> {
639        let admin = test_admin(1);
640        let minter_role = RoleSymbol::new("MINTER")?;
641        let minter_admin_role = RoleBasedAccessControl::admin_role();
642
643        let component: AccountComponent = RoleBasedAccessControl::builder()
644            .role(RoleConfig::new(minter_admin_role.clone()).with_member(admin))
645            .role(RoleConfig::new(minter_role.clone()).with_admin(minter_admin_role))
646            .build()?
647            .into();
648
649        // The role has a config entry but no members, so it does not exist yet.
650        let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
651        assert_eq!(
652            config.get(&role_config_key(&minter_role)),
653            Word::from([
654                Felt::ZERO,
655                RoleBasedAccessControl::admin_role().as_element(),
656                Felt::ZERO,
657                Felt::ZERO
658            ]),
659        );
660        let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
661        assert_eq!(membership.num_entries(), 1);
662
663        Ok(())
664    }
665
666    /// A role whose delegated admin is empty is still manageable as long as the admin itself can
667    /// be populated, which is the case while `ADMIN` is populated.
668    #[test]
669    fn delegating_to_a_role_populated_later_is_allowed() -> anyhow::Result<()> {
670        let admin = test_admin(1);
671        let minter_role = RoleSymbol::new("MINTER")?;
672        let minter_admin_role = RoleSymbol::new("MINTER_ADMIN")?;
673
674        RoleBasedAccessControl::builder()
675            .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(admin))
676            .role(RoleConfig::new(minter_role).with_admin(minter_admin_role))
677            .build()?;
678
679        Ok(())
680    }
681
682    #[rstest::rstest]
683    #[case::duplicate_role(
684        vec![
685            RoleConfig::new(role("MINTER")).with_member(test_admin(1)),
686            RoleConfig::new(role("MINTER")).with_member(test_admin(2)),
687        ],
688        RoleBasedAccessControlError::DuplicateRole(role("MINTER")),
689    )]
690    #[case::empty_config(
691        vec![RoleConfig::new(role("MINTER"))],
692        RoleBasedAccessControlError::EmptyRoleConfig(role("MINTER")),
693    )]
694    // MINTER delegates to a self-administering role that has no members, so nobody can ever
695    // populate MINTER_ADMIN and MINTER stays unmanageable.
696    #[case::unmanageable_role(
697        vec![
698            RoleConfig::new(role("MINTER")).with_admin(role("MINTER_ADMIN")),
699            RoleConfig::new(role("MINTER_ADMIN")).with_admin(role("MINTER_ADMIN")),
700        ],
701        RoleBasedAccessControlError::UnmanageableRole {
702            role: role("MINTER"),
703            admin: role("MINTER_ADMIN"),
704        },
705    )]
706    #[case::empty_admins(
707        vec![RoleConfig::new(RoleBasedAccessControl::admin_role())],
708        RoleBasedAccessControlError::EmptyRoleConfig(RoleBasedAccessControl::admin_role()),
709    )]
710    // Leaving MINTER's admin unset makes ADMIN administer it, but ADMIN administers itself, so an
711    // unspecified ADMIN can never hold members. This is the same defect as `unmanageable_role`,
712    // spelled implicitly.
713    #[case::unspecified_default_admin(
714        vec![RoleConfig::new(role("MINTER")).with_member(test_admin(1))],
715        RoleBasedAccessControlError::UnmanageableRole {
716            role: role("MINTER"),
717            admin: RoleBasedAccessControl::admin_role(),
718        },
719    )]
720    fn invalid_role_configs_are_rejected(
721        #[case] configs: Vec<RoleConfig>,
722        #[case] expected: RoleBasedAccessControlError,
723    ) {
724        let error = RoleBasedAccessControl::builder()
725            .roles(configs)
726            .build()
727            .expect_err("initialization should have failed");
728
729        assert_eq!(error, expected);
730    }
731}