use alloc::collections::BTreeSet;
use alloc::vec;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountComponent,
AccountComponentName,
AccountId,
RoleSymbol,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotName,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::account::account_component_code;
account_component_code!(RBAC_CODE, "miden-standards-access-rbac.masp");
static ROLE_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::access::rbac::role_config")
.expect("storage slot name should be valid")
});
static ROLE_MEMBERSHIP_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::access::rbac::role_membership")
.expect("storage slot name should be valid")
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoleBasedAccessControl {
initial_admins: BTreeSet<AccountId>,
}
impl RoleBasedAccessControl {
pub const NAME: &'static str = "miden::standards::components::access::rbac";
pub const ADMIN_ROLE: &'static str = "ADMIN";
pub fn admin_role() -> RoleSymbol {
RoleSymbol::new(Self::ADMIN_ROLE).expect("ADMIN is a valid role symbol")
}
pub const fn name() -> AccountComponentName {
AccountComponentName::from_static_str(Self::NAME)
}
pub fn code() -> &'static AccountComponentCode {
&RBAC_CODE
}
pub fn new(initial_admin: AccountId) -> Self {
Self {
initial_admins: BTreeSet::from([initial_admin]),
}
}
pub fn with_admins(initial_admins: BTreeSet<AccountId>) -> Self {
Self { initial_admins }
}
pub fn role_config_slot() -> &'static StorageSlotName {
&ROLE_CONFIG_SLOT_NAME
}
pub fn role_membership_slot() -> &'static StorageSlotName {
&ROLE_MEMBERSHIP_SLOT_NAME
}
pub fn role_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::role_config_slot().clone(),
StorageSlotSchema::map(
"Per-role RBAC configuration (member count and delegated admin role)",
SchemaType::role_symbol(),
SchemaType::native_word(),
),
)
}
pub fn role_membership_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
(
Self::role_membership_slot().clone(),
StorageSlotSchema::map(
"Role membership flag indexed by role symbol and account ID",
SchemaType::native_word(),
SchemaType::native_word(),
),
)
}
pub fn component_metadata() -> AccountComponentMetadata {
let storage_schema = StorageSchema::new(vec![
Self::role_config_slot_schema(),
Self::role_membership_slot_schema(),
])
.expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description("Role-based access control component")
.with_storage_schema(storage_schema)
}
}
impl From<RoleBasedAccessControl> for AccountComponent {
fn from(rbac: RoleBasedAccessControl) -> Self {
let admin_symbol: Felt = RoleBasedAccessControl::admin_role().as_element();
let admins = rbac.initial_admins;
let membership_entries = admins.iter().map(|admin| {
(
StorageMapKey::new(Word::from([
Felt::ZERO,
admin_symbol,
admin.suffix(),
admin.prefix().as_felt(),
])),
Word::from([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]),
)
});
let role_membership_map = StorageMap::with_entries(membership_entries)
.expect("seeded role membership map should be valid");
let role_config_map = if admins.is_empty() {
StorageMap::default()
} else {
let member_count =
u32::try_from(admins.len()).expect("number of initial admins should fit in u32");
StorageMap::with_entries(vec![(
StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, admin_symbol])),
Word::from([Felt::from(member_count), Felt::ZERO, Felt::ZERO, Felt::ZERO]),
)])
.expect("seeded role config map should be valid")
};
let role_config_slot = StorageSlot::with_map(
RoleBasedAccessControl::role_config_slot().clone(),
role_config_map,
);
let role_membership_slot = StorageSlot::with_map(
RoleBasedAccessControl::role_membership_slot().clone(),
role_membership_map,
);
AccountComponent::new(
RoleBasedAccessControl::code().clone(),
vec![role_config_slot, role_membership_slot],
RoleBasedAccessControl::component_metadata(),
)
.expect("RBAC component should satisfy the requirements of a valid account component")
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{AccountType, StorageSlotContent};
use super::*;
fn test_admin(seed: u8) -> AccountId {
AccountId::builder()
.account_type(AccountType::Private)
.build_with_seed([seed; 32])
}
fn find_map<'a>(
component: &'a AccountComponent,
slot_name: &StorageSlotName,
) -> &'a StorageMap {
let slot = component
.storage_slots()
.iter()
.find(|slot| slot.name() == slot_name)
.expect("component should register the slot");
match slot.content() {
StorageSlotContent::Map(map) => map,
_ => panic!("slot {slot_name} should be a map"),
}
}
#[test]
fn admin_role_encoding_matches_masm_constant() {
const MASM_ADMIN_ROLE: u64 = 1836707;
assert_eq!(
RoleBasedAccessControl::admin_role().as_element().as_canonical_u64(),
MASM_ADMIN_ROLE,
);
}
#[test]
fn with_admins_seeds_every_admin_and_the_member_count() {
let admins = [test_admin(1), test_admin(2), test_admin(3)];
let component: AccountComponent =
RoleBasedAccessControl::with_admins(admins.iter().copied().collect()).into();
let admin_symbol = RoleBasedAccessControl::admin_role().as_element();
let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
assert_eq!(membership.num_entries(), admins.len());
for admin in admins {
let key = StorageMapKey::new(Word::from([
Felt::ZERO,
admin_symbol,
admin.suffix(),
admin.prefix().as_felt(),
]));
assert_eq!(
membership.get(&key),
Word::from([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO])
);
}
let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
let config_key =
StorageMapKey::new(Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, admin_symbol]));
let member_count = u32::try_from(admins.len()).unwrap();
assert_eq!(
config.get(&config_key),
Word::from([Felt::from(member_count), Felt::ZERO, Felt::ZERO, Felt::ZERO]),
);
}
#[test]
fn with_admins_empty_seeds_no_admin() {
let component: AccountComponent =
RoleBasedAccessControl::with_admins(BTreeSet::new()).into();
let membership = find_map(&component, RoleBasedAccessControl::role_membership_slot());
assert_eq!(membership.num_entries(), 0);
let config = find_map(&component, RoleBasedAccessControl::role_config_slot());
assert_eq!(config.num_entries(), 0);
}
}