1use crate::response::ApiResponse;
2use axum::http::StatusCode;
3use std::{
4 ops::{Deref, DerefMut},
5 sync::{Arc, LazyLock},
6};
7
8#[derive(Clone)]
9pub enum AuthMethod {
10 Session(crate::models::user_session::UserSession),
11 ApiKey(crate::models::user_api_key::UserApiKey),
12}
13
14impl AuthMethod {
15 #[inline]
16 pub fn api_key_uuid(&self) -> Option<uuid::Uuid> {
17 match self {
18 Self::Session(_) => None,
19 Self::ApiKey(api_key) => Some(api_key.uuid),
20 }
21 }
22}
23
24#[derive(Clone)]
25pub struct UserImpersonator(pub super::User);
26
27impl Deref for UserImpersonator {
28 type Target = super::User;
29
30 fn deref(&self) -> &Self::Target {
31 &self.0
32 }
33}
34
35impl DerefMut for UserImpersonator {
36 fn deref_mut(&mut self) -> &mut Self::Target {
37 &mut self.0
38 }
39}
40
41pub type GetUser = crate::extract::ConsumingExtension<super::User>;
42pub type GetUserImpersonator = crate::extract::ConsumingExtension<Option<UserImpersonator>>;
43pub type GetAuthMethod = axum::extract::Extension<Arc<AuthMethod>>;
44pub type GetPermissionManager = axum::extract::Extension<PermissionManager>;
45
46static NO_PERMISSIONS: LazyLock<Arc<Vec<compact_str::CompactString>>> =
47 LazyLock::new(|| Arc::new(Vec::new()));
48
49#[derive(Clone)]
50pub enum CredentialScope {
51 Session,
52 ApiKey {
53 user: Arc<Vec<compact_str::CompactString>>,
54 admin: Arc<Vec<compact_str::CompactString>>,
55 server: Arc<Vec<compact_str::CompactString>>,
56 },
57}
58
59impl From<&AuthMethod> for CredentialScope {
60 fn from(auth: &AuthMethod) -> Self {
61 match auth {
62 AuthMethod::Session(_) => Self::Session,
63 AuthMethod::ApiKey(api_key) => Self::ApiKey {
64 user: Arc::clone(&api_key.user_permissions),
65 admin: Arc::clone(&api_key.admin_permissions),
66 server: Arc::clone(&api_key.server_permissions),
67 },
68 }
69 }
70}
71
72impl CredentialScope {
73 #[inline]
75 pub fn server_permissions(&self) -> Option<&[compact_str::CompactString]> {
76 match self {
77 Self::Session => None,
78 Self::ApiKey { server, .. } => Some(server),
79 }
80 }
81
82 #[inline]
83 fn allows(scope: Option<&[compact_str::CompactString]>, permission: &str) -> bool {
84 scope.is_none_or(|scope| scope.iter().any(|p| p == permission))
85 }
86
87 #[inline]
88 fn allows_user(&self, permission: &str) -> bool {
89 match self {
90 Self::Session => true,
91 Self::ApiKey { user, .. } => Self::allows(Some(user), permission),
92 }
93 }
94
95 #[inline]
96 fn allows_admin(&self, permission: &str) -> bool {
97 match self {
98 Self::Session => true,
99 Self::ApiKey { admin, .. } => Self::allows(Some(admin), permission),
100 }
101 }
102
103 #[inline]
104 fn allows_server(&self, permission: &str) -> bool {
105 Self::allows(self.server_permissions(), permission)
106 }
107
108 #[inline]
109 pub fn is_session(&self) -> bool {
110 matches!(self, Self::Session)
111 }
112
113 pub fn covers(
116 &self,
117 user: &[compact_str::CompactString],
118 admin: &[compact_str::CompactString],
119 server: &[compact_str::CompactString],
120 ) -> bool {
121 user.iter().all(|p| self.allows_user(p))
122 && admin.iter().all(|p| self.allows_admin(p))
123 && server.iter().all(|p| self.allows_server(p))
124 }
125}
126
127#[derive(Clone)]
128pub struct PermissionManager {
129 user_uuid: uuid::Uuid,
130 user_admin: bool,
131 role_admin_permissions: Arc<Vec<compact_str::CompactString>>,
132 role_server_permissions: Arc<Vec<compact_str::CompactString>>,
133 server_owner: bool,
134 server_subuser_permissions: Arc<Vec<compact_str::CompactString>>,
135 scope: CredentialScope,
136 impersonated: bool,
137}
138
139impl PermissionManager {
140 pub fn new(user: &super::User, scope: CredentialScope) -> Self {
141 Self {
142 user_uuid: user.uuid,
143 user_admin: user.admin,
144 role_admin_permissions: user.role.as_ref().map_or_else(
145 || Arc::clone(&NO_PERMISSIONS),
146 |role| Arc::clone(&role.admin_permissions),
147 ),
148 role_server_permissions: user.role.as_ref().map_or_else(
149 || Arc::clone(&NO_PERMISSIONS),
150 |role| Arc::clone(&role.server_permissions),
151 ),
152 server_owner: false,
153 server_subuser_permissions: Arc::clone(&NO_PERMISSIONS),
154 scope,
155 impersonated: false,
156 }
157 }
158
159 pub fn for_impersonation(self) -> Self {
160 Self {
161 impersonated: true,
162 ..self
163 }
164 }
165
166 pub fn for_server(&self, server: &crate::models::server::Server) -> Self {
169 Self {
170 server_owner: self.user_uuid == server.owner.uuid,
171 server_subuser_permissions: server
172 .subuser_permissions
173 .as_ref()
174 .map_or_else(|| Arc::clone(&NO_PERMISSIONS), Arc::clone),
175 ..self.clone()
176 }
177 }
178
179 #[inline]
180 pub fn user_uuid(&self) -> uuid::Uuid {
181 self.user_uuid
182 }
183
184 #[inline]
185 pub fn scope(&self) -> &CredentialScope {
186 &self.scope
187 }
188
189 #[inline]
190 fn check(allowed: bool, permission: &str) -> Result<(), ApiResponse> {
191 if allowed {
192 return Ok(());
193 }
194
195 Err(ApiResponse::error(format!(
196 "you do not have permission to perform this action: {permission}"
197 ))
198 .with_status(StatusCode::FORBIDDEN))
199 }
200
201 pub fn has_user_permission(&self, permission: &str) -> Result<(), ApiResponse> {
204 Self::check(self.scope.allows_user(permission), permission)
205 }
206
207 pub fn has_admin_permission(&self, permission: &str) -> Result<(), ApiResponse> {
208 let granted =
209 self.user_admin || self.role_admin_permissions.iter().any(|p| p == permission);
210
211 Self::check(granted && self.scope.allows_admin(permission), permission)
212 }
213
214 pub fn can_grant(
217 &self,
218 admin: &[compact_str::CompactString],
219 server: &[compact_str::CompactString],
220 ) -> bool {
221 admin.iter().all(|permission| {
222 (self.user_admin || self.role_admin_permissions.contains(permission))
223 && self.scope.allows_admin(permission)
224 }) && server.iter().all(|permission| {
225 (self.user_admin || self.role_server_permissions.contains(permission))
226 && self.scope.allows_server(permission)
227 })
228 }
229
230 pub fn deny_while_impersonating(&self) -> Result<(), ApiResponse> {
233 if !self.impersonated {
234 return Ok(());
235 }
236
237 Err(
238 ApiResponse::error("this cannot be modified while impersonating")
239 .with_status(StatusCode::CONFLICT),
240 )
241 }
242
243 pub fn can_impersonate(&self, user: &super::User) -> bool {
246 if user.admin {
247 return self.is_unrestricted_admin();
248 }
249
250 user.role
251 .as_ref()
252 .is_none_or(|role| self.can_grant(&role.admin_permissions, &role.server_permissions))
253 }
254
255 pub fn is_unrestricted_admin(&self) -> bool {
260 self.user_admin && self.scope.is_session()
261 }
262
263 pub fn can_modify_user(&self, user: &super::User) -> Result<(), ApiResponse> {
266 if self.is_unrestricted_admin() || !user.admin {
267 return Ok(());
268 }
269
270 Err(ApiResponse::error("you cannot modify an administrator")
271 .with_status(StatusCode::FORBIDDEN))
272 }
273
274 pub fn has_server_permission(&self, permission: &str) -> Result<(), ApiResponse> {
275 let granted = self.user_admin
276 || self.server_owner
277 || self.role_server_permissions.iter().any(|p| p == permission)
278 || self
279 .server_subuser_permissions
280 .iter()
281 .any(|p| p == permission);
282
283 Self::check(granted && self.scope.allows_server(permission), permission)
284 }
285
286 pub fn missing_server_permission<T: AsRef<str>>(
287 &self,
288 permissions: impl IntoIterator<Item = T>,
289 ) -> Option<T> {
290 permissions
291 .into_iter()
292 .find(|permission| self.has_server_permission(permission.as_ref()).is_err())
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn permissions(permissions: &[&str]) -> Arc<Vec<compact_str::CompactString>> {
301 Arc::new(permissions.iter().map(|p| (*p).into()).collect())
302 }
303
304 fn manager(scope: CredentialScope) -> PermissionManager {
305 PermissionManager {
306 user_uuid: uuid::Uuid::nil(),
307 user_admin: false,
308 role_admin_permissions: Arc::clone(&NO_PERMISSIONS),
309 role_server_permissions: Arc::clone(&NO_PERMISSIONS),
310 server_owner: false,
311 server_subuser_permissions: Arc::clone(&NO_PERMISSIONS),
312 scope,
313 impersonated: false,
314 }
315 }
316
317 fn api_key(user: &[&str], admin: &[&str], server: &[&str]) -> CredentialScope {
318 CredentialScope::ApiKey {
319 user: permissions(user),
320 admin: permissions(admin),
321 server: permissions(server),
322 }
323 }
324
325 fn user(admin: bool, role: Option<(&[&str], &[&str])>) -> super::super::User {
326 super::super::User {
327 uuid: uuid::Uuid::new_v4(),
328 role: role.map(|(admin_permissions, server_permissions)| {
329 serde_json::from_value(serde_json::json!({
330 "uuid": uuid::Uuid::new_v4(),
331 "name": "role",
332 "description": null,
333 "require_two_factor": false,
334 "admin_permissions": admin_permissions,
335 "server_permissions": server_permissions,
336 "created": chrono::NaiveDateTime::default(),
337 "extension_data": [],
338 }))
339 .unwrap()
340 }),
341 external_id: None,
342 avatar: None,
343 username: "user".into(),
344 email: "user@example.com".into(),
345 name_first: None,
346 name_last: None,
347 admin,
348 frozen: false,
349 suspended: false,
350 totp_enabled: false,
351 totp_last_used: None,
352 totp_secret: None,
353 email_two_factor_enabled: false,
354 has_security_key: false,
355 email_verified: true,
356 password_login_disabled: false,
357 language: "en".into(),
358 has_password: true,
359 created: chrono::NaiveDateTime::default(),
360 extension_data: Vec::new(),
361 }
362 }
363
364 #[test]
365 fn api_key_scope_constrains_a_full_admin() {
366 let mut manager = manager(api_key(&[], &["users.impersonate"], &[]));
367 manager.user_admin = true;
368
369 assert!(manager.has_admin_permission("users.impersonate").is_ok());
370 assert!(manager.has_admin_permission("roles.read").is_err());
371 assert!(manager.has_admin_permission("extensions.manage").is_err());
372 }
373
374 #[test]
375 fn an_unconfigured_api_key_denies_everything() {
376 let mut manager = manager(api_key(&[], &[], &[]));
377 manager.user_admin = true;
378 manager.server_owner = true;
379
380 assert!(manager.has_user_permission("account.infos").is_err());
381 assert!(manager.has_admin_permission("roles.read").is_err());
382 assert!(manager.has_server_permission("files.read").is_err());
383 }
384
385 #[test]
386 fn a_session_holds_every_user_permission() {
387 let manager = manager(CredentialScope::Session);
388
389 assert!(manager.has_user_permission("account.infos").is_ok());
390 assert!(manager.has_user_permission("api-keys.create").is_ok());
391 }
392
393 #[test]
394 fn a_session_still_defers_to_account_grants() {
395 let manager = manager(CredentialScope::Session);
396
397 assert!(manager.has_admin_permission("roles.read").is_err());
398 assert!(manager.has_server_permission("files.read").is_err());
399 }
400
401 #[test]
402 fn a_grant_and_a_scope_are_intersected() {
403 let mut manager = manager(api_key(&[], &["roles.read"], &[]));
404 manager.role_admin_permissions = permissions(&["roles.read", "nodes.read"]);
405
406 assert!(manager.has_admin_permission("roles.read").is_ok());
407 assert!(manager.has_admin_permission("nodes.read").is_err());
408 assert!(manager.has_admin_permission("users.read").is_err());
409 }
410
411 #[test]
412 fn a_scope_alone_grants_nothing() {
413 let manager = manager(api_key(&[], &["roles.read"], &["files.read"]));
414
415 assert!(manager.has_admin_permission("roles.read").is_err());
416 assert!(manager.has_server_permission("files.read").is_err());
417 }
418
419 #[test]
420 fn subuser_grants_are_scoped_too() {
421 let mut manager = manager(api_key(&[], &[], &["files.read"]));
422 manager.server_subuser_permissions = permissions(&["files.read", "files.delete"]);
423
424 assert!(manager.has_server_permission("files.read").is_ok());
425 assert!(manager.has_server_permission("files.delete").is_err());
426 }
427
428 #[test]
429 fn server_ownership_is_scoped_too() {
430 let mut manager = manager(api_key(&[], &[], &["files.read"]));
431 manager.server_owner = true;
432
433 assert!(manager.has_server_permission("files.read").is_ok());
434 assert!(manager.has_server_permission("files.delete").is_err());
435 }
436
437 #[test]
439 fn only_a_session_is_a_session() {
440 assert!(CredentialScope::Session.is_session());
441 assert!(!api_key(&["account.infos"], &["roles.read"], &["files.read"]).is_session());
442 }
443
444 #[test]
446 fn granting_nothing_is_always_allowed() {
447 assert!(manager(CredentialScope::Session).can_grant(&[], &[]));
448 assert!(manager(api_key(&[], &[], &[])).can_grant(&[], &[]));
449 }
450
451 #[test]
452 fn an_admin_session_can_grant_anything() {
453 let mut manager = manager(CredentialScope::Session);
454 manager.user_admin = true;
455
456 assert!(manager.can_grant(&["users.update".into()], &["files.delete".into()]));
457 }
458
459 #[test]
460 fn an_admin_api_key_can_only_grant_what_it_lists() {
461 let mut manager = manager(api_key(&[], &["roles.read"], &["files.read"]));
462 manager.user_admin = true;
463
464 assert!(manager.can_grant(&["roles.read".into()], &["files.read".into()]));
465 assert!(!manager.can_grant(&["roles.read".into(), "users.update".into()], &[]));
466 assert!(!manager.can_grant(&[], &["files.delete".into()]));
467 }
468
469 #[test]
470 fn role_grants_bound_what_can_be_granted() {
471 let mut manager = manager(CredentialScope::Session);
472 manager.role_admin_permissions = permissions(&["roles.read"]);
473 manager.role_server_permissions = permissions(&["files.read"]);
474
475 assert!(manager.can_grant(&["roles.read".into()], &["files.read".into()]));
476 assert!(!manager.can_grant(&["users.update".into()], &[]));
477 assert!(!manager.can_grant(&[], &["files.delete".into()]));
478 }
479
480 #[test]
481 fn server_ownership_and_subuser_grants_cannot_be_handed_on() {
482 let mut manager = manager(CredentialScope::Session);
483 manager.server_owner = true;
484 manager.server_subuser_permissions = permissions(&["files.read"]);
485
486 assert!(!manager.can_grant(&[], &["files.read".into()]));
487 assert!(!manager.can_grant(&[], &["files.delete".into()]));
488 }
489
490 #[test]
492 fn only_an_admin_session_can_impersonate_an_admin() {
493 let mut session = manager(CredentialScope::Session);
494 session.user_admin = true;
495 let mut key = manager(api_key(
496 &["account.infos"],
497 &["users.impersonate"],
498 &["files.read"],
499 ));
500 key.user_admin = true;
501
502 assert!(session.can_impersonate(&user(true, None)));
503 assert!(!key.can_impersonate(&user(true, None)));
504 assert!(!manager(CredentialScope::Session).can_impersonate(&user(true, None)));
505 }
506
507 #[test]
508 fn a_target_without_a_role_can_be_impersonated() {
509 assert!(manager(CredentialScope::Session).can_impersonate(&user(false, None)));
510 assert!(manager(api_key(&[], &[], &[])).can_impersonate(&user(false, None)));
511 }
512
513 #[test]
514 fn a_target_role_must_be_grantable_by_the_caller() {
515 let mut manager = manager(api_key(
516 &[],
517 &["roles.read", "users.impersonate"],
518 &["files.read"],
519 ));
520 manager.user_admin = true;
521
522 assert!(manager.can_impersonate(&user(false, Some((&["roles.read"], &["files.read"])))));
523 assert!(!manager.can_impersonate(&user(false, Some((&["users.update"], &[])))));
524 assert!(!manager.can_impersonate(&user(false, Some((&[], &["files.delete"])))));
525 }
526
527 #[test]
528 fn a_non_admin_cannot_impersonate_a_stronger_role() {
529 let mut manager = manager(CredentialScope::Session);
530 manager.role_admin_permissions = permissions(&["users.impersonate"]);
531
532 assert!(manager.can_impersonate(&user(false, Some((&["users.impersonate"], &[])))));
533 assert!(!manager.can_impersonate(&user(
534 false,
535 Some((&["users.impersonate", "users.update"], &[]))
536 )));
537 }
538
539 #[test]
541 fn only_an_admin_session_is_unrestricted() {
542 let mut session = manager(CredentialScope::Session);
543 session.user_admin = true;
544 let mut key = manager(api_key(
545 &["account.infos"],
546 &["roles.read", "servers.read", "users.update"],
547 &["files.read"],
548 ));
549 key.user_admin = true;
550 let mut role = manager(CredentialScope::Session);
551 role.role_admin_permissions = permissions(&["roles.read", "servers.read", "users.update"]);
552
553 assert!(session.is_unrestricted_admin());
554 assert!(!key.is_unrestricted_admin());
555 assert!(!role.is_unrestricted_admin());
556 }
557
558 #[test]
560 fn impersonation_is_denied_with_a_conflict() {
561 assert!(
562 manager(CredentialScope::Session)
563 .deny_while_impersonating()
564 .is_ok()
565 );
566 assert_eq!(
567 manager(CredentialScope::Session)
568 .for_impersonation()
569 .deny_while_impersonating()
570 .unwrap_err()
571 .status,
572 StatusCode::CONFLICT
573 );
574 }
575
576 #[test]
578 fn impersonation_keeps_the_same_grants() {
579 let mut manager = manager(api_key(
580 &["account.infos"],
581 &["roles.read"],
582 &["files.read"],
583 ));
584 manager.role_admin_permissions = permissions(&["roles.read", "nodes.read"]);
585 manager.role_server_permissions = permissions(&["files.read", "files.delete"]);
586 let manager = manager.for_impersonation();
587
588 assert!(manager.has_user_permission("account.infos").is_ok());
589 assert!(manager.has_user_permission("api-keys.create").is_err());
590 assert!(manager.has_admin_permission("roles.read").is_ok());
591 assert!(manager.has_admin_permission("nodes.read").is_err());
592 assert!(manager.has_server_permission("files.read").is_ok());
593 assert!(manager.has_server_permission("files.delete").is_err());
594 }
595
596 #[test]
598 fn anyone_may_modify_a_non_admin() {
599 assert!(
600 manager(CredentialScope::Session)
601 .can_modify_user(&user(false, Some((&["roles.read"], &[]))))
602 .is_ok()
603 );
604 assert!(
605 manager(api_key(&[], &[], &[]))
606 .can_modify_user(&user(false, None))
607 .is_ok()
608 );
609 }
610
611 #[test]
612 fn only_an_admin_session_may_modify_an_admin() {
613 let mut session = manager(CredentialScope::Session);
614 session.user_admin = true;
615 let mut key = manager(api_key(&["account.infos"], &["users.update"], &[]));
616 key.user_admin = true;
617 let mut role = manager(CredentialScope::Session);
618 role.role_admin_permissions = permissions(&["users.update"]);
619
620 assert!(session.can_modify_user(&user(true, None)).is_ok());
621 assert_eq!(
622 key.can_modify_user(&user(true, None)).unwrap_err().status,
623 StatusCode::FORBIDDEN
624 );
625 assert_eq!(
626 role.can_modify_user(&user(true, None)).unwrap_err().status,
627 StatusCode::FORBIDDEN
628 );
629 }
630
631 #[test]
633 fn nothing_is_missing_when_everything_is_held() {
634 let mut manager = manager(CredentialScope::Session);
635 manager.server_subuser_permissions = permissions(&["files.read"]);
636 manager.role_server_permissions = permissions(&["files.delete"]);
637
638 assert_eq!(
639 manager.missing_server_permission(["files.read", "files.delete"]),
640 None
641 );
642 assert_eq!(manager.missing_server_permission::<&str>([]), None);
643 }
644
645 #[test]
646 fn the_first_missing_permission_is_reported() {
647 let mut manager = manager(CredentialScope::Session);
648 manager.server_subuser_permissions = permissions(&["files.read"]);
649
650 assert_eq!(
651 manager.missing_server_permission(["files.read", "files.delete", "files.create"]),
652 Some("files.delete")
653 );
654 }
655
656 #[test]
657 fn missing_permissions_respect_the_scope() {
658 let mut manager = manager(api_key(&[], &[], &["files.read"]));
659 manager.server_owner = true;
660
661 assert_eq!(manager.missing_server_permission(["files.read"]), None);
662 assert_eq!(
663 manager.missing_server_permission(["files.read", "files.delete"]),
664 Some("files.delete")
665 );
666 }
667}