1use crate::{memory_id, stable_storage, IcarusStorable};
7use candid::{CandidType, Deserialize, Principal};
8use ic_stable_structures::memory_manager::VirtualMemory;
9use ic_stable_structures::{DefaultMemoryImpl, StableBTreeMap};
10use serde::Serialize;
11
12type Memory = VirtualMemory<DefaultMemoryImpl>;
13
14#[derive(Debug, Clone, Serialize, Deserialize, CandidType, IcarusStorable)]
16#[icarus_storable(unbounded)]
17pub struct User {
18 pub principal: Principal,
19 pub added_at: u64,
20 pub added_by: Principal,
21 pub role: AuthRole,
22 pub active: bool,
23 pub last_access: Option<u64>,
24 pub access_count: u64,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CandidType)]
29pub enum AuthRole {
30 Owner, Admin, User, ReadOnly, }
35
36#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
38pub struct AuthInfo {
39 pub principal: String,
40 pub role: AuthRole,
41 pub is_authenticated: bool,
42 pub last_access: Option<u64>,
43 pub access_count: u64,
44 pub message: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, CandidType, IcarusStorable)]
49#[icarus_storable(unbounded)]
50pub struct AuthAuditEntry {
51 pub id: String,
52 pub timestamp: u64,
53 pub action: AuthAction,
54 pub principal: Principal,
55 pub target_principal: Option<Principal>,
56 pub performed_by: Principal,
57 pub success: bool,
58 pub details: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
63pub enum AuthAction {
64 AddUser,
65 RemoveUser,
66 UpdateRole,
67 DeactivateUser,
68 ReactivateUser,
69 AccessGranted,
70 AccessDenied,
71 ViewAuditLog,
72}
73
74stable_storage! {
76 AUTH_USERS: StableBTreeMap<Principal, User, Memory> = memory_id!(10);
77 AUTH_AUDIT: StableBTreeMap<String, AuthAuditEntry, Memory> = memory_id!(11);
78 AUTH_COUNTER: u64 = 0;
79}
80
81pub fn init_auth(owner: Principal) {
83 if owner == Principal::anonymous() {
85 ic_cdk::trap("Security Error: Anonymous principal cannot be set as owner");
86 }
87
88 AUTH_USERS.with(|users| {
89 let owner_entry = User {
90 principal: owner,
91 added_at: ic_cdk::api::time(),
92 added_by: owner, role: AuthRole::Owner,
94 active: true,
95 last_access: None,
96 access_count: 0,
97 };
98 users.borrow_mut().insert(owner, owner_entry);
99 });
100
101 log_auth_action(
102 AuthAction::AddUser,
103 owner,
104 Some(owner),
105 owner,
106 true,
107 "Initial owner setup".to_string(),
108 );
109}
110
111pub fn authenticate() -> AuthInfo {
113 let caller = ic_cdk::api::msg_caller();
114
115 if caller == Principal::anonymous() {
117 ic_cdk::trap("Access denied: Anonymous principal cannot be authenticated");
118 }
119
120 AUTH_USERS.with(|users| {
121 let mut auth_entry = if let Some(entry) = users.borrow().get(&caller) {
123 entry.clone()
124 } else {
125 log_auth_action(
126 AuthAction::AccessDenied,
127 caller,
128 None,
129 caller,
130 false,
131 "Principal not found in authorized users".to_string(),
132 );
133
134 ic_cdk::trap(format!(
135 "Access denied: Principal {} not authorized",
136 caller.to_text()
137 ));
138 };
139
140 if !auth_entry.active {
141 log_auth_action(
142 AuthAction::AccessDenied,
143 caller,
144 None,
145 caller,
146 false,
147 "User account deactivated".to_string(),
148 );
149
150 ic_cdk::trap("Access denied: account deactivated");
151 }
152
153 auth_entry.last_access = Some(ic_cdk::api::time());
155 auth_entry.access_count += 1;
156 users.borrow_mut().insert(caller, auth_entry.clone());
157
158 log_auth_action(
159 AuthAction::AccessGranted,
160 caller,
161 None,
162 caller,
163 true,
164 format!("Access granted with role: {:?}", auth_entry.role),
165 );
166
167 AuthInfo {
168 principal: caller.to_text(),
169 role: auth_entry.role,
170 is_authenticated: true,
171 last_access: auth_entry.last_access,
172 access_count: auth_entry.access_count,
173 message: "Access granted".to_string(),
174 }
175 })
176}
177
178pub fn require_role_or_higher(minimum_role: AuthRole) -> AuthInfo {
181 let auth_info = authenticate();
182
183 let has_permission = matches!(
184 (&auth_info.role, &minimum_role),
185 (AuthRole::Owner, _)
186 | (
187 AuthRole::Admin,
188 AuthRole::Admin | AuthRole::User | AuthRole::ReadOnly
189 )
190 | (AuthRole::User, AuthRole::User | AuthRole::ReadOnly)
191 | (AuthRole::ReadOnly, AuthRole::ReadOnly)
192 );
193
194 if has_permission {
195 auth_info
196 } else {
197 ic_cdk::trap(format!(
198 "Insufficient permissions: {:?} or higher required",
199 minimum_role
200 ));
201 }
202}
203
204pub fn require_exact_role(role: AuthRole) -> AuthInfo {
206 let auth_info = authenticate();
207 if matches!(auth_info.role, ref r if *r == role) {
208 auth_info
209 } else {
210 ic_cdk::trap(format!(
211 "Requires exactly {:?} role, but caller has {:?}",
212 role, auth_info.role
213 ));
214 }
215}
216
217pub fn require_any_of_roles(roles: &[AuthRole]) -> AuthInfo {
219 let auth_info = authenticate();
220 if roles.contains(&auth_info.role) {
221 auth_info
222 } else {
223 ic_cdk::trap(format!(
224 "Requires one of: {:?}, but caller has {:?}",
225 roles, auth_info.role
226 ));
227 }
228}
229
230pub fn require_none_of_roles(excluded: &[AuthRole]) -> AuthInfo {
232 let auth_info = authenticate();
233 if !excluded.contains(&auth_info.role) {
234 auth_info
235 } else {
236 ic_cdk::trap(format!("Role {:?} is not allowed here", auth_info.role));
237 }
238}
239
240pub fn add_user(principal: Principal, role: AuthRole) -> String {
242 if principal == Principal::anonymous() {
244 ic_cdk::trap("Security Error: Anonymous principal cannot be authorized");
245 }
246
247 let auth_info = require_role_or_higher(AuthRole::Admin);
248 let caller = ic_cdk::api::msg_caller();
249
250 if matches!(role, AuthRole::Owner) && !matches!(auth_info.role, AuthRole::Owner) {
252 ic_cdk::trap("Only owners can create other owners");
253 }
254
255 AUTH_USERS.with(|users| {
256 if users.borrow().contains_key(&principal) {
257 ic_cdk::trap("Principal already authorized");
258 }
259
260 let auth_entry = User {
261 principal,
262 added_at: ic_cdk::api::time(),
263 added_by: caller,
264 role: role.clone(),
265 active: true,
266 last_access: None,
267 access_count: 0,
268 };
269
270 users.borrow_mut().insert(principal, auth_entry);
271
272 log_auth_action(
273 AuthAction::AddUser,
274 principal,
275 Some(principal),
276 caller,
277 true,
278 format!("User added with role: {:?}", role),
279 );
280
281 format!(
282 "Principal {} added with role {:?} by {}",
283 principal.to_text(),
284 role,
285 caller.to_text()
286 )
287 })
288}
289
290pub fn remove_user(principal: Principal) -> String {
292 let auth_info = require_role_or_higher(AuthRole::Admin);
293 let caller = ic_cdk::api::msg_caller();
294
295 AUTH_USERS.with(|users| {
296 let should_remove = {
298 if let Some(target_entry) = users.borrow().get(&principal) {
299 if matches!(target_entry.role, AuthRole::Owner)
301 && !matches!(auth_info.role, AuthRole::Owner)
302 {
303 ic_cdk::trap("Only owners can remove other owners");
304 }
305
306 if principal == caller {
308 ic_cdk::trap("Cannot remove yourself");
309 }
310
311 true
312 } else {
313 false
314 }
315 }; if should_remove {
319 users.borrow_mut().remove(&principal);
320
321 log_auth_action(
322 AuthAction::RemoveUser,
323 principal,
324 Some(principal),
325 caller,
326 true,
327 format!("User removed by {}", caller.to_text()),
328 );
329
330 format!(
331 "Principal {} removed by {}",
332 principal.to_text(),
333 caller.to_text()
334 )
335 } else {
336 ic_cdk::trap(format!("User {} not found", principal.to_text()))
337 }
338 })
339}
340
341pub fn update_user_role(principal: Principal, new_role: AuthRole) -> String {
343 if principal == Principal::anonymous() {
345 ic_cdk::trap("Security Error: Anonymous principal cannot have a role");
346 }
347
348 require_role_or_higher(AuthRole::Owner); let caller = ic_cdk::api::msg_caller();
350
351 AUTH_USERS.with(|users| {
352 let auth_entry_opt = users.borrow().get(&principal);
354
355 if let Some(mut auth_entry) = auth_entry_opt {
356 let old_role = auth_entry.role.clone();
357 auth_entry.role = new_role.clone();
358 users.borrow_mut().insert(principal, auth_entry);
359
360 log_auth_action(
361 AuthAction::UpdateRole,
362 principal,
363 Some(principal),
364 caller,
365 true,
366 format!("Role changed from {:?} to {:?}", old_role, new_role),
367 );
368
369 format!(
370 "Principal {} role updated from {:?} to {:?}",
371 principal.to_text(),
372 old_role,
373 new_role
374 )
375 } else {
376 ic_cdk::trap("Principal not found");
377 }
378 })
379}
380
381pub fn get_authorized_users() -> Vec<User> {
383 require_role_or_higher(AuthRole::Admin);
384 let caller = ic_cdk::api::msg_caller();
385
386 log_auth_action(
387 AuthAction::ViewAuditLog,
388 caller,
389 None,
390 caller,
391 true,
392 "Viewed authorized users list".to_string(),
393 );
394
395 AUTH_USERS.with(|users| {
396 users
397 .borrow()
398 .iter()
399 .map(|entry| entry.value().clone())
400 .collect()
401 })
402}
403
404pub fn get_auth_audit(limit: Option<u32>) -> Vec<AuthAuditEntry> {
406 require_role_or_higher(AuthRole::Owner);
407 let caller = ic_cdk::api::msg_caller();
408 let limit = limit.unwrap_or(100).min(1000) as usize;
409
410 log_auth_action(
411 AuthAction::ViewAuditLog,
412 caller,
413 None,
414 caller,
415 true,
416 format!("Viewed audit log (limit: {})", limit),
417 );
418
419 AUTH_AUDIT.with(|audit| {
420 let mut entries: Vec<AuthAuditEntry> = audit
421 .borrow()
422 .iter()
423 .map(|entry| entry.value().clone())
424 .collect();
425
426 entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
428 entries.truncate(limit);
429
430 entries
431 })
432}
433
434fn log_auth_action(
436 action: AuthAction,
437 principal: Principal,
438 target_principal: Option<Principal>,
439 performed_by: Principal,
440 success: bool,
441 details: String,
442) {
443 let audit_id = AUTH_COUNTER.with(|c| {
444 let current = *c.borrow();
445 *c.borrow_mut() = current + 1;
446 let timestamp = ic_cdk::api::time() / 1_000_000;
447 format!("audit_{}_{}", timestamp, current + 1)
448 });
449
450 let audit_entry = AuthAuditEntry {
451 id: audit_id.clone(),
452 timestamp: ic_cdk::api::time(),
453 action,
454 principal,
455 target_principal,
456 performed_by,
457 success,
458 details,
459 };
460
461 AUTH_AUDIT.with(|audit| {
462 audit.borrow_mut().insert(audit_id, audit_entry);
463 });
464}
465
466pub fn get_auth_status() -> AuthInfo {
468 authenticate()
469}
470
471pub fn list_users() -> Vec<User> {
473 get_authorized_users()
474}
475
476pub fn get_user(principal: Principal) -> Option<User> {
478 AUTH_USERS.with(|users| users.borrow().get(&principal))
479}
480
481#[macro_export]
483macro_rules! require_auth {
484 () => {
485 $crate::auth::authenticate();
486 };
487}
488
489#[macro_export]
490macro_rules! require_admin {
491 () => {
492 $crate::auth::require_role_or_higher($crate::auth::AuthRole::Admin);
493 };
494}
495
496#[macro_export]
497macro_rules! require_owner {
498 () => {
499 $crate::auth::require_role_or_higher($crate::auth::AuthRole::Owner);
500 };
501}