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