Skip to main content

miden_standards/account/access/
authority.rs

1use miden_protocol::account::component::{
2    AccountComponentCode,
3    AccountComponentMetadata,
4    FeltSchema,
5    StorageSchema,
6    StorageSlotSchema,
7};
8use miden_protocol::account::{
9    AccountComponent,
10    AccountStorage,
11    RoleSymbol,
12    StorageSlot,
13    StorageSlotName,
14};
15use miden_protocol::errors::{AccountError, RoleSymbolError};
16use miden_protocol::utils::sync::LazyLock;
17use miden_protocol::{Felt, Word};
18use thiserror::Error;
19
20use crate::account::account_component_code;
21
22// CONSTANTS
23// ================================================================================================
24
25account_component_code!(AUTHORITY_CODE, "access/authority.masl");
26
27static AUTHORITY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
28    StorageSlotName::new("miden::standards::access::authority")
29        .expect("storage slot name should be valid")
30});
31
32/// Authority value written to the storage slot for [`Authority::AuthControlled`].
33const AUTH_CONTROLLED: u8 = 0;
34/// Authority value written to the storage slot for [`Authority::OwnerControlled`].
35const OWNER_CONTROLLED: u8 = 1;
36/// Authority value written to the storage slot for [`Authority::RbacControlled`].
37const RBAC_CONTROLLED: u8 = 2;
38
39// AUTHORITY
40// ================================================================================================
41
42/// Identifies which authority is allowed to invoke an authority-gated procedure on an account.
43///
44/// Components that gate state-mutating procedures (such as
45/// [`TokenPolicyManager`][crate::account::policies::TokenPolicyManager] for `set_mint_policy` /
46/// `set_burn_policy`, or the fungible token metadata setters) consult this single shared slot via
47/// the MASM helper `authority::assert_authorized`. Installing the [`Authority`] component on an
48/// account thus selects the gating mode for *all* such procedures in one place.
49///
50/// # Safety invariant for [`Authority::AuthControlled`]
51///
52/// Because `assert_authorized` is a no-op under `AuthControlled`, the account's auth component
53/// is the **sole** gate for every authority-gated setter. The auth component MUST therefore
54/// authenticate every such setter root, otherwise the setters become permissionless.
55///
56/// Storage layout: `[authority, role_symbol_or_zero, 0, 0]` — single Word.
57#[repr(u8)]
58#[derive(Debug, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum Authority {
61    /// Authority is the account's auth component; no extra check is performed by
62    /// `authority::assert_authorized`.
63    AuthControlled = AUTH_CONTROLLED,
64    /// Authority is the [`Ownable2Step`][crate::account::access::Ownable2Step] owner; the call
65    /// must be sent by the registered owner.
66    OwnerControlled = OWNER_CONTROLLED,
67    /// Authority is membership in a specific RBAC role. The call must be sent by an account that
68    /// holds `role` in the
69    /// [`RoleBasedAccessControl`][crate::account::access::RoleBasedAccessControl] component.
70    ///
71    /// Requires the [`RoleBasedAccessControl`][crate::account::access::RoleBasedAccessControl]
72    /// component to be installed on the account; the MASM helper calls into
73    /// `rbac::assert_sender_has_role` and will fail to link otherwise.
74    RbacControlled { role: RoleSymbol } = RBAC_CONTROLLED,
75}
76
77impl Authority {
78    /// The name of the component.
79    pub const NAME: &'static str = "miden::standards::components::access::authority";
80
81    /// Returns the [`AccountComponentCode`] of this component.
82    pub fn code() -> &'static AccountComponentCode {
83        &AUTHORITY_CODE
84    }
85
86    // PUBLIC ACCESSORS
87    // --------------------------------------------------------------------------------------------
88
89    /// Returns the [`StorageSlotName`] holding the authority configuration.
90    pub fn authority_slot() -> &'static StorageSlotName {
91        &AUTHORITY_SLOT_NAME
92    }
93
94    /// Reads the authority configuration from account storage.
95    pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, AuthorityError> {
96        let word = storage
97            .get_item(Self::authority_slot())
98            .map_err(AuthorityError::MissingStorageSlot)?;
99        Self::try_from(word)
100    }
101
102    /// Returns the [`AccountComponentMetadata`] for this component.
103    pub fn component_metadata() -> AccountComponentMetadata {
104        let storage_schema = StorageSchema::new(vec![(
105            AUTHORITY_SLOT_NAME.clone(),
106            StorageSlotSchema::value(
107                "Authority configuration",
108                [
109                    FeltSchema::u8("authority"),
110                    FeltSchema::felt("role_symbol"),
111                    FeltSchema::new_void(),
112                    FeltSchema::new_void(),
113                ],
114            ),
115        )])
116        .expect("storage schema should be valid");
117
118        AccountComponentMetadata::new(Self::NAME)
119            .with_description(
120                "Account-wide authority shared by procedures that gate state-mutating \
121                 operations behind auth-only, owner-based, or RBAC role-based checks",
122            )
123            .with_storage_schema(storage_schema)
124    }
125}
126
127// TRAIT IMPLEMENTATIONS
128// ================================================================================================
129
130impl From<Authority> for Word {
131    fn from(value: Authority) -> Self {
132        match value {
133            Authority::AuthControlled => {
134                Word::new([Felt::from(AUTH_CONTROLLED), Felt::ZERO, Felt::ZERO, Felt::ZERO])
135            },
136            Authority::OwnerControlled => {
137                Word::new([Felt::from(OWNER_CONTROLLED), Felt::ZERO, Felt::ZERO, Felt::ZERO])
138            },
139            Authority::RbacControlled { role } => {
140                Word::new([Felt::from(RBAC_CONTROLLED), role.into(), Felt::ZERO, Felt::ZERO])
141            },
142        }
143    }
144}
145
146impl TryFrom<Word> for Authority {
147    type Error = AuthorityError;
148
149    fn try_from(word: Word) -> Result<Self, Self::Error> {
150        let authority: u8 = word[0]
151            .as_canonical_u64()
152            .try_into()
153            .map_err(|_| AuthorityError::InvalidAuthority(word[0].as_canonical_u64()))?;
154        match authority {
155            AUTH_CONTROLLED => Ok(Self::AuthControlled),
156            OWNER_CONTROLLED => Ok(Self::OwnerControlled),
157            RBAC_CONTROLLED => {
158                let role =
159                    RoleSymbol::try_from(word[1]).map_err(AuthorityError::InvalidRoleSymbol)?;
160                Ok(Self::RbacControlled { role })
161            },
162            other => Err(AuthorityError::InvalidAuthority(other.into())),
163        }
164    }
165}
166
167impl From<Authority> for AccountComponent {
168    fn from(value: Authority) -> Self {
169        let slot = StorageSlot::with_value(AUTHORITY_SLOT_NAME.clone(), Word::from(value));
170        AccountComponent::new(
171            Authority::code().clone(),
172            vec![slot],
173            Authority::component_metadata(),
174        )
175        .expect("authority component should satisfy the requirements of a valid account component")
176    }
177}
178
179// AUTHORITY ERROR
180// ================================================================================================
181
182/// Errors raised when reading or parsing an [`Authority`] from storage.
183#[derive(Debug, Error)]
184pub enum AuthorityError {
185    #[error("invalid authority value: {0}")]
186    InvalidAuthority(u64),
187    #[error("invalid role symbol in authority slot")]
188    InvalidRoleSymbol(#[source] RoleSymbolError),
189    #[error("failed to read authority slot from storage")]
190    MissingStorageSlot(#[source] AccountError),
191}