templar_curator_primitives/rbac/
mod.rs

1//! RBAC (Role-Based Access Control) auth adapter for curator vaults.
2//!
3//! This module provides an RBAC implementation of the [`AuthAdapter`] trait
4//! for curator vaults. It enforces role-based access control where different
5//! roles have permission to perform different actions.
6//!
7//! # Roles
8//!
9//! - **Curator**: Curator-scoped actions, plus allocator-class operations
10//! - **Sentinel**: Emergency backstop (used for pause and restriction updates)
11//! - **Allocator**: Can manage allocations and refreshes
12//! - **User**: Can deposit, withdraw, execute withdrawals
13
14use alloc::vec::Vec;
15use templar_vault_kernel::Address;
16
17use crate::auth::{
18    canonical_policy_class, ActionKind, AuthAdapter, AuthError, AuthPolicyClass, AuthResult,
19};
20
21/// Role types for RBAC.
22#[templar_vault_macros::vault_derive(borsh, schemars, serde)]
23#[derive(Clone, Copy, PartialEq, Eq)]
24#[cfg_attr(feature = "boundary", derive(near_sdk::BorshStorageKey))]
25pub enum Role {
26    /// Curator-scoped privileged actions (and allocator-class operations).
27    Curator,
28    /// Emergency backstop (used for pause and restriction updates).
29    Sentinel,
30    /// Can manage allocations and market operations.
31    Allocator,
32}
33
34impl Role {
35    #[cfg(not(target_arch = "wasm32"))]
36    #[inline]
37    #[must_use]
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Role::Curator => "curator",
41            Role::Sentinel => "sentinel",
42            Role::Allocator => "allocator",
43        }
44    }
45}
46
47/// Role assignment for an address.
48#[templar_vault_macros::vault_derive(borsh, serde)]
49#[derive(Clone, PartialEq, Eq)]
50pub struct RoleAssignment {
51    /// The address with this role.
52    pub address: Address,
53    /// The assigned role.
54    pub role: Role,
55}
56
57/// RBAC configuration for the vault.
58#[templar_vault_macros::vault_derive]
59#[derive(Clone, Default)]
60pub struct RbacConfig {
61    /// List of role assignments.
62    pub assignments: Vec<RoleAssignment>,
63    /// Whether the vault is paused.
64    pub paused: bool,
65}
66
67impl RbacConfig {
68    /// Create an RBAC configuration with a single curator.
69    #[inline]
70    #[must_use]
71    pub fn with_curator(curator: Address) -> Self {
72        Self {
73            assignments: alloc::vec![RoleAssignment {
74                address: curator,
75                role: Role::Curator,
76            }],
77            paused: false,
78        }
79    }
80
81    /// Add a role assignment.
82    #[inline]
83    pub fn add_role(&mut self, address: Address, role: Role) {
84        // Remove any existing assignment for this address with the same role
85        self.assignments
86            .retain(|a| !(a.address == address && a.role == role));
87        self.assignments.push(RoleAssignment { address, role });
88    }
89
90    /// Remove a role from an address.
91    #[inline]
92    pub fn remove_role(&mut self, address: &Address, role: Role) {
93        self.assignments
94            .retain(|assignment| !(assignment.address == *address && assignment.role == role));
95    }
96
97    /// Check if an address has a specific role.
98    #[inline]
99    #[must_use]
100    pub fn has_role(&self, address: &Address, role: Role) -> bool {
101        self.assignments
102            .iter()
103            .any(|assignment| assignment.address == *address && assignment.role == role)
104    }
105
106    #[inline]
107    #[must_use]
108    fn is_curator(&self, address: &Address) -> bool {
109        self.has_role(address, Role::Curator)
110    }
111
112    /// Get all roles for an address.
113    #[must_use]
114    pub fn get_roles(&self, address: &Address) -> Vec<Role> {
115        self.assignments
116            .iter()
117            .filter(|a| &a.address == address)
118            .map(|a| a.role)
119            .collect()
120    }
121
122    /// Set the paused state.
123    #[inline]
124    pub fn set_paused(&mut self, paused: bool) {
125        self.paused = paused;
126    }
127}
128
129/// Get the required role for an action.
130///
131/// This is the canonical action-to-role mapping shared across all executors.
132/// Returns `None` for user-facing actions that don't require a special role.
133#[inline]
134#[must_use]
135pub fn required_role(action: ActionKind) -> Option<Role> {
136    match canonical_policy_class(action) {
137        AuthPolicyClass::Public => None,
138        AuthPolicyClass::Sentinel => Some(Role::Sentinel),
139        AuthPolicyClass::Allocator | AuthPolicyClass::AllocatorEmergency => Some(Role::Allocator),
140        AuthPolicyClass::Curator => Some(Role::Curator),
141    }
142}
143
144/// RBAC auth adapter implementation.
145///
146/// This adapter enforces role-based access control for curator vault actions.
147/// It checks that the caller has the required role for each action type.
148#[templar_vault_macros::vault_derive]
149#[derive(Clone, Default)]
150pub struct RbacAuth {
151    /// RBAC configuration.
152    pub config: RbacConfig,
153}
154
155impl RbacAuth {
156    #[inline]
157    fn is_allowed(&self, action: ActionKind, caller: &Address) -> bool {
158        match canonical_policy_class(action) {
159            AuthPolicyClass::Public => true,
160            AuthPolicyClass::Sentinel => self.config.has_role(caller, Role::Sentinel),
161            AuthPolicyClass::Allocator => {
162                self.config.has_role(caller, Role::Allocator) || self.config.is_curator(caller)
163            }
164            AuthPolicyClass::AllocatorEmergency => {
165                self.config.has_role(caller, Role::Allocator)
166                    || self.config.has_role(caller, Role::Sentinel)
167                    || self.config.is_curator(caller)
168            }
169            AuthPolicyClass::Curator => self.config.is_curator(caller),
170        }
171    }
172}
173
174impl AuthAdapter for RbacAuth {
175    fn authorize(
176        &self,
177        action: ActionKind,
178        caller: Address,
179        _proof: Option<&[u8]>,
180    ) -> AuthResult<()> {
181        // Check if paused (allow pause action even when paused).
182        // Public/user actions are blocked while paused.
183        if self.config.paused && action != ActionKind::Pause && !action.is_privileged() {
184            return Err(AuthError::VaultPaused);
185        }
186
187        if !self.is_allowed(action, &caller) {
188            return Err(AuthError::MissingRole);
189        }
190
191        Ok(())
192    }
193
194    fn is_paused(&self) -> bool {
195        self.config.paused
196    }
197}