koprogo_api/application/use_cases/
role_delegation_use_cases.rs1use crate::application::error::AppError;
18use crate::application::ports::RoleDelegationRepository;
19use crate::domain::entities::{UserRole, UserRoleAssignment};
20use chrono::{DateTime, Duration, Utc};
21use std::sync::Arc;
22use uuid::Uuid;
23
24pub const MAX_DELEGATION_DAYS: i64 = 90;
27
28pub struct RoleDelegationUseCases {
29 repo: Arc<dyn RoleDelegationRepository>,
30}
31
32impl RoleDelegationUseCases {
33 pub fn new(repo: Arc<dyn RoleDelegationRepository>) -> Self {
34 Self { repo }
35 }
36
37 pub async fn delegate_role(
44 &self,
45 delegator_user_id: Uuid,
46 target_user_id: Uuid,
47 role: UserRole,
48 organization_id: Option<Uuid>,
49 valid_until: DateTime<Utc>,
50 ) -> Result<UserRoleAssignment, AppError> {
51 if delegator_user_id == target_user_id {
53 return Err(AppError::Validation(
54 "Cannot delegate a role to oneself".to_string(),
55 ));
56 }
57 if delegator_user_id.is_nil() || target_user_id.is_nil() {
58 return Err(AppError::Validation(
59 "Delegation user ids must not be nil UUIDs".to_string(),
60 ));
61 }
62
63 let now = Utc::now();
65 if valid_until <= now {
66 return Err(AppError::Validation(
67 "Delegation valid_until must be strictly in the future".to_string(),
68 ));
69 }
70 let duration = valid_until - now;
71 if duration > Duration::days(MAX_DELEGATION_DAYS) {
72 return Err(AppError::Validation(format!(
73 "Delegation duration exceeds {} days (anti-abuse)",
74 MAX_DELEGATION_DAYS
75 )));
76 }
77
78 let delegator_assignments = self
81 .repo
82 .find_active_by_user_and_role(delegator_user_id, &role, organization_id)
83 .await?;
84 let has_native = delegator_assignments
85 .iter()
86 .any(|a| !a.is_delegated() && a.is_currently_active());
87 if !has_native {
88 return Err(AppError::DelegationChainNotAllowed);
91 }
92
93 let target_assignments = self
95 .repo
96 .find_active_by_user_and_role(target_user_id, &role, organization_id)
97 .await?;
98 if target_assignments.iter().any(|a| a.is_currently_active()) {
99 return Err(AppError::RoleAlreadyAssigned {
100 user_id: target_user_id,
101 role: role.to_string(),
102 });
103 }
104
105 let assignment = UserRoleAssignment::new_delegated(
107 target_user_id,
108 role,
109 organization_id,
110 valid_until,
111 delegator_user_id,
112 );
113 self.repo.save(&assignment).await?;
114 Ok(assignment)
115 }
116
117 pub async fn revoke_delegation(&self, assignment_id: Uuid) -> Result<(), AppError> {
119 let existing =
120 self.repo.find_by_id(assignment_id).await?.ok_or_else(|| {
121 AppError::NotFound(format!("Delegation {} not found", assignment_id))
122 })?;
123 if !existing.is_delegated() {
124 return Err(AppError::Validation(
125 "Assignment is not a delegation".to_string(),
126 ));
127 }
128 self.repo.revoke(assignment_id).await?;
129 Ok(())
130 }
131
132 pub async fn list_delegations_of(
134 &self,
135 user_id: Uuid,
136 ) -> Result<Vec<UserRoleAssignment>, AppError> {
137 self.repo.list_delegations_of(user_id).await
138 }
139}
140
141#[cfg(test)]
146mod tests {
147 use super::*;
148 use async_trait::async_trait;
149 use std::sync::Mutex;
150
151 #[derive(Default)]
152 struct InMemoryRepo {
153 rows: Mutex<Vec<UserRoleAssignment>>,
154 }
155
156 impl InMemoryRepo {
157 fn push(&self, a: UserRoleAssignment) {
158 self.rows.lock().unwrap().push(a);
159 }
160 }
161
162 #[async_trait]
163 impl RoleDelegationRepository for InMemoryRepo {
164 async fn save(&self, a: &UserRoleAssignment) -> Result<(), AppError> {
165 self.rows.lock().unwrap().push(a.clone());
166 Ok(())
167 }
168
169 async fn find_by_id(&self, id: Uuid) -> Result<Option<UserRoleAssignment>, AppError> {
170 Ok(self
171 .rows
172 .lock()
173 .unwrap()
174 .iter()
175 .find(|a| a.id == id)
176 .cloned())
177 }
178
179 async fn find_active_by_user_and_role(
180 &self,
181 user_id: Uuid,
182 role: &UserRole,
183 organization_id: Option<Uuid>,
184 ) -> Result<Vec<UserRoleAssignment>, AppError> {
185 Ok(self
186 .rows
187 .lock()
188 .unwrap()
189 .iter()
190 .filter(|a| {
191 a.user_id == user_id
192 && &a.role == role
193 && a.organization_id == organization_id
194 && a.is_currently_active()
195 })
196 .cloned()
197 .collect())
198 }
199
200 async fn list_delegations_of(
201 &self,
202 user_id: Uuid,
203 ) -> Result<Vec<UserRoleAssignment>, AppError> {
204 Ok(self
205 .rows
206 .lock()
207 .unwrap()
208 .iter()
209 .filter(|a| {
210 a.is_delegated()
211 && a.is_currently_active()
212 && (a.user_id == user_id || a.delegated_from_user_id == Some(user_id))
213 })
214 .cloned()
215 .collect())
216 }
217
218 async fn revoke(&self, id: Uuid) -> Result<(), AppError> {
219 self.rows.lock().unwrap().retain(|a| a.id != id);
220 Ok(())
221 }
222 }
223
224 fn factory() -> (Arc<InMemoryRepo>, RoleDelegationUseCases) {
225 let repo: Arc<InMemoryRepo> = Arc::new(InMemoryRepo::default());
226 let uc = RoleDelegationUseCases::new(repo.clone() as Arc<dyn RoleDelegationRepository>);
227 (repo, uc)
228 }
229
230 fn seed_native(repo: &InMemoryRepo, user: Uuid, role: UserRole) {
232 repo.push(UserRoleAssignment::new(user, role, None, true));
233 }
234
235 #[tokio::test]
238 async fn happy_delegate_then_list_finds_assignment() {
239 let (repo, uc) = factory();
240 let syndic = Uuid::new_v4();
241 let owner = Uuid::new_v4();
242 seed_native(&repo, syndic, UserRole::Syndic);
243
244 let valid_until = Utc::now() + Duration::days(7);
245 let delegation = uc
246 .delegate_role(syndic, owner, UserRole::Syndic, None, valid_until)
247 .await
248 .expect("delegate ok");
249
250 assert_eq!(delegation.user_id, owner);
251 assert_eq!(delegation.delegated_from_user_id, Some(syndic));
252 assert!(delegation.is_delegated());
253 assert!(delegation.is_currently_active());
254
255 let received = uc.list_delegations_of(owner).await.unwrap();
257 assert_eq!(received.len(), 1);
258 assert_eq!(received[0].id, delegation.id);
259
260 let granted = uc.list_delegations_of(syndic).await.unwrap();
262 assert_eq!(granted.len(), 1);
263 assert_eq!(granted[0].id, delegation.id);
264 }
265
266 #[tokio::test]
267 async fn happy_revoke_delegation_removes_it() {
268 let (repo, uc) = factory();
269 let syndic = Uuid::new_v4();
270 let owner = Uuid::new_v4();
271 seed_native(&repo, syndic, UserRole::Syndic);
272
273 let valid_until = Utc::now() + Duration::days(7);
274 let d = uc
275 .delegate_role(syndic, owner, UserRole::Syndic, None, valid_until)
276 .await
277 .unwrap();
278 uc.revoke_delegation(d.id).await.unwrap();
279 assert!(uc.list_delegations_of(owner).await.unwrap().is_empty());
280 }
281
282 #[tokio::test]
285 async fn edge_valid_until_exactly_now_is_rejected() {
286 let (repo, uc) = factory();
287 let syndic = Uuid::new_v4();
288 let owner = Uuid::new_v4();
289 seed_native(&repo, syndic, UserRole::Syndic);
290 let valid_until = Utc::now() - Duration::seconds(1);
294 let err = uc
295 .delegate_role(syndic, owner, UserRole::Syndic, None, valid_until)
296 .await
297 .unwrap_err();
298 assert!(matches!(err, AppError::Validation(_)));
299 }
300
301 #[tokio::test]
302 async fn edge_max_window_minus_epsilon_is_accepted() {
303 let (repo, uc) = factory();
304 let syndic = Uuid::new_v4();
305 let owner = Uuid::new_v4();
306 seed_native(&repo, syndic, UserRole::Syndic);
307 let valid_until = Utc::now() + Duration::days(MAX_DELEGATION_DAYS) - Duration::seconds(5);
310 let res = uc
311 .delegate_role(syndic, owner, UserRole::Syndic, None, valid_until)
312 .await;
313 assert!(res.is_ok(), "{:?}", res.err());
314 }
315
316 #[tokio::test]
319 async fn security_self_delegation_is_rejected() {
320 let (repo, uc) = factory();
321 let same = Uuid::new_v4();
322 seed_native(&repo, same, UserRole::Syndic);
323 let err = uc
324 .delegate_role(
325 same,
326 same,
327 UserRole::Syndic,
328 None,
329 Utc::now() + Duration::days(7),
330 )
331 .await
332 .unwrap_err();
333 assert!(matches!(err, AppError::Validation(_)));
334 }
335
336 #[tokio::test]
337 async fn security_non_transitive_delegated_role_cannot_be_redelegated() {
338 let (repo, uc) = factory();
341 let original_syndic = Uuid::new_v4();
342 let owner_a = Uuid::new_v4();
343 let owner_b = Uuid::new_v4();
344 seed_native(&repo, original_syndic, UserRole::Syndic);
345
346 let valid_until = Utc::now() + Duration::days(7);
347 let _ = uc
348 .delegate_role(
349 original_syndic,
350 owner_a,
351 UserRole::Syndic,
352 None,
353 valid_until,
354 )
355 .await
356 .expect("first delegation ok");
357
358 let err = uc
360 .delegate_role(owner_a, owner_b, UserRole::Syndic, None, valid_until)
361 .await
362 .unwrap_err();
363 assert!(
364 matches!(err, AppError::DelegationChainNotAllowed),
365 "expected DelegationChainNotAllowed, got {:?}",
366 err
367 );
368 }
369
370 #[tokio::test]
371 async fn security_delegator_without_any_role_is_rejected() {
372 let (_repo, uc) = factory();
375 let stranger = Uuid::new_v4();
376 let target = Uuid::new_v4();
377 let err = uc
378 .delegate_role(
379 stranger,
380 target,
381 UserRole::Syndic,
382 None,
383 Utc::now() + Duration::days(7),
384 )
385 .await
386 .unwrap_err();
387 assert!(matches!(err, AppError::DelegationChainNotAllowed));
388 }
389
390 #[tokio::test]
391 async fn security_nil_ids_are_rejected() {
392 let (_repo, uc) = factory();
393 let err = uc
394 .delegate_role(
395 Uuid::nil(),
396 Uuid::new_v4(),
397 UserRole::Syndic,
398 None,
399 Utc::now() + Duration::days(7),
400 )
401 .await
402 .unwrap_err();
403 assert!(matches!(err, AppError::Validation(_)));
404 }
405
406 #[tokio::test]
409 async fn negative_duration_above_max_is_rejected() {
410 let (repo, uc) = factory();
411 let syndic = Uuid::new_v4();
412 let target = Uuid::new_v4();
413 seed_native(&repo, syndic, UserRole::Syndic);
414
415 let valid_until = Utc::now() + Duration::days(MAX_DELEGATION_DAYS + 1);
416 let err = uc
417 .delegate_role(syndic, target, UserRole::Syndic, None, valid_until)
418 .await
419 .unwrap_err();
420 assert!(matches!(err, AppError::Validation(_)));
421 }
422
423 #[tokio::test]
424 async fn negative_target_already_has_role_returns_conflict() {
425 let (repo, uc) = factory();
426 let syndic_a = Uuid::new_v4();
427 let target = Uuid::new_v4();
428 seed_native(&repo, syndic_a, UserRole::Syndic);
429 seed_native(&repo, target, UserRole::Syndic);
431
432 let err = uc
433 .delegate_role(
434 syndic_a,
435 target,
436 UserRole::Syndic,
437 None,
438 Utc::now() + Duration::days(7),
439 )
440 .await
441 .unwrap_err();
442 assert!(
443 matches!(err, AppError::RoleAlreadyAssigned { .. }),
444 "expected RoleAlreadyAssigned, got {:?}",
445 err
446 );
447 }
448
449 #[tokio::test]
450 async fn negative_revoke_unknown_id_returns_not_found() {
451 let (_repo, uc) = factory();
452 let err = uc.revoke_delegation(Uuid::new_v4()).await.unwrap_err();
453 assert!(matches!(err, AppError::NotFound(_)));
454 }
455
456 #[tokio::test]
457 async fn negative_revoke_native_assignment_is_rejected() {
458 let (repo, uc) = factory();
459 let user = Uuid::new_v4();
460 let native = UserRoleAssignment::new(user, UserRole::Syndic, None, true);
461 let native_id = native.id;
462 repo.push(native);
463 let err = uc.revoke_delegation(native_id).await.unwrap_err();
466 assert!(matches!(err, AppError::Validation(_)));
467 }
468}