templar_curator_primitives/rbac/
mod.rs1use 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#[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,
28 Sentinel,
30 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#[templar_vault_macros::vault_derive(borsh, serde)]
86#[derive(Clone, PartialEq, Eq)]
87pub struct RoleAssignment {
88 pub address: Address,
90 pub role: Role,
92}
93
94#[templar_vault_macros::vault_derive]
96#[derive(Clone, Default)]
97pub struct RbacConfig {
98 assignments: Vec<RoleAssignment>,
99 paused: bool,
101}
102
103impl RbacConfig {
104 #[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 #[inline]
119 #[must_use]
120 pub fn with_curator(curator: Address) -> Self {
121 Self::new(curator)
122 }
123
124 #[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 #[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 #[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 #[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 #[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#[templar_vault_macros::vault_derive]
226#[derive(Clone)]
227pub struct RbacAuth {
228 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}