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(¤t) {
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}