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
13use alloc::vec::Vec;
14use templar_vault_kernel::Address;
15
16use crate::auth::{
17    allowed_while_paused, canonical_policy_class, ActionKind, AuthAdapter, AuthError,
18    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#[derive(Clone, Copy, PartialEq, Eq)]
48struct RoleSet(u8);
49
50impl RoleSet {
51    const NONE: Self = Self(0);
52    const CURATOR: Self = Self(1 << 0);
53    const SENTINEL: Self = Self(1 << 1);
54    const ALLOCATOR: Self = Self(1 << 2);
55
56    #[inline]
57    const fn union(self, other: Self) -> Self {
58        Self(self.0 | other.0)
59    }
60
61    #[inline]
62    const fn contains(self, role: Role) -> bool {
63        let mask = match role {
64            Role::Curator => Self::CURATOR.0,
65            Role::Sentinel => Self::SENTINEL.0,
66            Role::Allocator => Self::ALLOCATOR.0,
67        };
68        self.0 & mask != 0
69    }
70}
71
72const fn allowed_roles_for(action: ActionKind) -> RoleSet {
73    match canonical_policy_class(action) {
74        AuthPolicyClass::Public => RoleSet::NONE,
75        AuthPolicyClass::Sentinel => RoleSet::SENTINEL,
76        AuthPolicyClass::Allocator => RoleSet::ALLOCATOR.union(RoleSet::CURATOR),
77        AuthPolicyClass::AllocatorEmergency => RoleSet::ALLOCATOR
78            .union(RoleSet::SENTINEL)
79            .union(RoleSet::CURATOR),
80        AuthPolicyClass::Curator => RoleSet::CURATOR,
81    }
82}
83
84/// Role assignment for an address.
85#[templar_vault_macros::vault_derive(borsh, serde)]
86#[derive(Clone, PartialEq, Eq)]
87pub struct RoleAssignment {
88    /// The address with this role.
89    pub address: Address,
90    /// The assigned role.
91    pub role: Role,
92}
93
94/// RBAC configuration for the vault.
95#[templar_vault_macros::vault_derive]
96#[derive(Clone, Default)]
97pub struct RbacConfig {
98    assignments: Vec<RoleAssignment>,
99    /// Whether the vault is paused.
100    paused: bool,
101}
102
103impl RbacConfig {
104    /// Create an RBAC configuration with a single curator.
105    #[inline]
106    #[must_use]
107    pub fn new(curator: Address) -> Self {
108        Self {
109            assignments: alloc::vec![RoleAssignment {
110                address: curator,
111                role: Role::Curator,
112            }],
113            paused: false,
114        }
115    }
116
117    /// Create an RBAC configuration with a single curator.
118    #[inline]
119    #[must_use]
120    pub fn with_curator(curator: Address) -> Self {
121        Self::new(curator)
122    }
123
124    /// Add a role assignment.
125    #[inline]
126    pub fn add_role(&mut self, address: Address, role: Role) -> bool {
127        if self.has_role(&address, role) {
128            return false;
129        }
130
131        self.assignments.push(RoleAssignment { address, role });
132        true
133    }
134
135    /// Remove a role from an address.
136    #[inline]
137    pub fn remove_role(&mut self, address: &Address, role: Role) -> bool {
138        if role == Role::Curator && self.curator_count() == 1 && self.has_role(address, role) {
139            return false;
140        }
141
142        let original_len = self.assignments.len();
143        self.assignments
144            .retain(|assignment| assignment.address != *address || assignment.role != role);
145
146        if self.assignments.len() == original_len {
147            return false;
148        }
149
150        true
151    }
152
153    /// Check if an address has a specific role.
154    #[inline]
155    #[must_use]
156    pub fn has_role(&self, address: &Address, role: Role) -> bool {
157        self.role_set_for(address).contains(role)
158    }
159
160    #[inline]
161    #[must_use]
162    fn role_set_for(&self, address: &Address) -> RoleSet {
163        self.assignments
164            .iter()
165            .filter(|assignment| assignment.address == *address)
166            .fold(RoleSet::NONE, |roles, assignment| {
167                roles.union(match assignment.role {
168                    Role::Curator => RoleSet::CURATOR,
169                    Role::Sentinel => RoleSet::SENTINEL,
170                    Role::Allocator => RoleSet::ALLOCATOR,
171                })
172            })
173    }
174
175    #[inline]
176    #[must_use]
177    fn curator_count(&self) -> usize {
178        self.assignments
179            .iter()
180            .filter(|assignment| assignment.role == Role::Curator)
181            .count()
182    }
183
184    /// Get all roles for an address.
185    #[must_use]
186    pub fn get_roles(&self, address: &Address) -> Vec<Role> {
187        let roles = self.role_set_for(address);
188        [Role::Curator, Role::Sentinel, Role::Allocator]
189            .into_iter()
190            .filter(|role| roles.contains(*role))
191            .collect()
192    }
193
194    #[must_use]
195    pub fn role_assignments(&self) -> Vec<RoleAssignment> {
196        self.assignments.clone()
197    }
198
199    /// Set the paused state.
200    #[inline]
201    pub fn set_paused(&mut self, paused: bool) {
202        self.paused = paused;
203    }
204
205    #[inline]
206    #[must_use]
207    pub fn is_paused(&self) -> bool {
208        self.paused
209    }
210}
211
212#[inline]
213#[must_use]
214pub fn allowed_roles_for_action(action: ActionKind) -> Vec<Role> {
215    [Role::Curator, Role::Sentinel, Role::Allocator]
216        .into_iter()
217        .filter(|role| allowed_roles_for(action).contains(*role))
218        .collect()
219}
220
221/// RBAC auth adapter implementation.
222///
223/// This adapter enforces role-based access control for curator vault actions.
224/// It checks that the caller has the required role for each action type.
225#[templar_vault_macros::vault_derive]
226#[derive(Clone)]
227pub struct RbacAuth {
228    /// RBAC configuration.
229    config: RbacConfig,
230}
231
232impl RbacAuth {
233    #[inline]
234    #[must_use]
235    pub fn new(config: RbacConfig) -> Self {
236        Self { config }
237    }
238
239    #[inline]
240    #[must_use]
241    pub fn config(&self) -> &RbacConfig {
242        &self.config
243    }
244
245    #[inline]
246    pub fn set_paused(&mut self, paused: bool) {
247        self.config.set_paused(paused);
248    }
249
250    #[inline]
251    fn is_allowed(&self, caller: &Address, allowed_roles: RoleSet) -> bool {
252        let caller_roles = self.config.role_set_for(caller);
253        allowed_roles == RoleSet::NONE
254            || [Role::Curator, Role::Sentinel, Role::Allocator]
255                .into_iter()
256                .any(|role| allowed_roles.contains(role) && caller_roles.contains(role))
257    }
258}
259
260impl AuthAdapter for RbacAuth {
261    fn authorize(
262        &self,
263        action: ActionKind,
264        caller: Address,
265        _proof: Option<&[u8]>,
266    ) -> AuthResult<()> {
267        if self.config.is_paused() && !allowed_while_paused(action) {
268            return Err(AuthError::VaultPaused);
269        }
270
271        if !self.is_allowed(&caller, allowed_roles_for(action)) {
272            return Err(AuthError::MissingRole {
273                action,
274                policy_class: canonical_policy_class(action),
275            });
276        }
277
278        Ok(())
279    }
280
281    fn is_paused(&self) -> bool {
282        self.config.is_paused()
283    }
284}