Skip to main content

shared/models/user/
auth.rs

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    /// The server permissions this credential is limited to, or [`None`] when it is unscoped.
74    #[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    /// Whether this credential is scoped to every given permission, so it cannot hand out more
114    /// than it holds itself (e.g. when an api key creates another api key).
115    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    /// Resolves the grants the user has on a specific server, replacing those of any server this
167    /// manager was previously scoped to.
168    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    /// User permissions cover self-service on one's own account, which every account holds
202    /// unconditionally, so the credential scope is the only thing that can withhold them.
203    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    /// Whether this credential holds every given permission itself, so handing them to someone
215    /// else (through a role, an oauth mapping or an account link) cannot escalate.
216    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    /// Credentials and settings changed while impersonating would outlive the impersonation and
231    /// act outside its audit trail, so only the account itself may change them.
232    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    /// Administrators may only be impersonated by an administrator signed in with a session;
244    /// anyone else may only be impersonated by a caller who could grant their whole role.
245    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    /// Actions that could turn the caller into a full administrator (granting admin status,
256    /// writing to or impersonating administrators, linking identities, changing how an oauth
257    /// provider identifies users) need an administrator signed in with a session, because an api
258    /// key scoped to a single admin permission would otherwise be enough.
259    pub fn is_unrestricted_admin(&self) -> bool {
260        self.user_admin && self.scope.is_session()
261    }
262
263    /// Administrators may only be written to by other administrators signed in with a session,
264    /// no matter which admin permissions a role or api key grants.
265    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    // CredentialScope::is_session
438    #[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    // PermissionManager::can_grant
445    #[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    // PermissionManager::can_impersonate
491    #[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    // PermissionManager::is_unrestricted_admin
540    #[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    // PermissionManager::deny_while_impersonating
559    #[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    // PermissionManager::for_impersonation
577    #[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    // PermissionManager::can_modify_user
597    #[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    // PermissionManager::missing_server_permission
632    #[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}