Skip to main content

alibi_plugins/admin/
mod.rs

1pub mod access;
2
3mod callbacks;
4
5pub(super) mod handlers;
6
7pub(super) mod types;
8
9mod validation;
10
11use crate::helpers::{delete_session_cookie_headers, get_cookie};
12pub use access::RolePermissions;
13use access::{has_permission, is_admin_role, is_admin_user_id};
14use alibi_core::entity::AuthUser;
15use alibi_core::utils::cookie_utils::{
16    create_clear_cookie, create_session_cookie_with_max_age, create_session_like_cookie,
17    related_cookie_name,
18};
19use alibi_core::utils::username::{UsernameValidationError, validate_username};
20use alibi_core::wire::{SessionView, UserView};
21use alibi_core::{
22    AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, ErrorCodeMessageResponse,
23};
24pub(crate) use callbacks::BannedUserMessagePolicy;
25pub use callbacks::{AdminBannedUserMessage, AdminBannedUserMessageHandler};
26use handlers::AdminDateOperationError;
27use handlers::{
28    AdminSessionCookiePayload, ban_user_core, create_admin_session_cookie_value, create_user_core,
29    decode_admin_session_cookie_value, get_user_core, has_permission_core, impersonate_user_core,
30    list_user_sessions_core, list_users_core, remove_user_core, revoke_user_session_core,
31    revoke_user_sessions_core, set_role_core, set_user_password_core, stop_impersonating_core,
32    unban_user_core, update_user_core,
33};
34use std::collections::HashMap;
35use types::{
36    AdminUpdateUserRequest, BanUserRequest, CreateUserRequest, HasPermissionRequest,
37    ListUsersQueryParams, RevokeSessionRequest, SetRoleRequest, SetUserPasswordRequest,
38    UserIdRequest,
39};
40
41const MESSAGE_CHANGE_ROLE: &str = "You are not allowed to change users role";
42
43const MESSAGE_CREATE_USERS: &str = "You are not allowed to create users";
44
45const MESSAGE_LIST_USERS: &str = "You are not allowed to list users";
46
47const MESSAGE_LIST_USER_SESSIONS: &str = "You are not allowed to list users sessions";
48
49const MESSAGE_BAN_USERS: &str = "You are not allowed to ban users";
50
51const MESSAGE_IMPERSONATE_USERS: &str = "You are not allowed to impersonate users";
52
53const MESSAGE_REVOKE_USER_SESSIONS: &str = "You are not allowed to revoke users sessions";
54
55const MESSAGE_DELETE_USERS: &str = "You are not allowed to delete users";
56
57const MESSAGE_SET_USER_PASSWORD: &str = "You are not allowed to set users password";
58
59const MESSAGE_GET_USER: &str = "You are not allowed to get user";
60
61const MESSAGE_UPDATE_USERS: &str = "You are not allowed to update users";
62
63const MESSAGE_USERNAME_IS_ALREADY_TAKEN: &str = "Username is already taken. Please try another.";
64
65const MESSAGE_USERNAME_TOO_SHORT: &str = "Username is too short";
66
67const MESSAGE_USERNAME_TOO_LONG: &str = "Username is too long";
68
69const MESSAGE_INVALID_USERNAME: &str = "Username is invalid";
70
71/// Admin plugin for user management operations.
72pub struct AdminPlugin {
73    config: AdminConfig,
74}
75
76/// Configuration for the admin plugin.
77#[derive(Debug, Clone, alibi_core::PluginConfig)]
78#[plugin(name = "AdminPlugin")]
79pub struct AdminConfig {
80    /// Default role assigned to new users and role-less permission checks.
81    #[config(default = "user".to_owned())]
82    pub default_role: String,
83    /// Roles treated as "admin" for target-admin checks such as impersonation.
84    /// None uses the default admin role; explicit lists are validated at initialization.
85    #[config(default = None)]
86    pub admin_roles: Option<Vec<String>>,
87    /// Users that always bypass admin permission checks.
88    #[config(default = None)]
89    pub admin_user_ids: Option<Vec<String>>,
90    /// Custom role definitions. When provided, these replace the built-in
91    /// `admin` and `user` role permissions. None uses builtins; Some(empty) grants none.
92    #[config(default = None)]
93    pub roles: Option<HashMap<String, RolePermissions>>,
94    /// Default reason applied when banning a user without an explicit reason.
95    #[config(default = None)]
96    pub default_ban_reason: Option<String>,
97    /// Default ban duration in seconds, including fractions; zero/NaN act as unset.
98    #[config(default = None)]
99    pub default_ban_expires_in: Option<f64>,
100    /// Custom impersonation lifetime in seconds, including fractions; zero/NaN use one hour.
101    #[config(default = None)]
102    pub impersonation_session_duration: Option<f64>,
103    /// Message surfaced to banned users.
104    #[config(default = "You have been banned from this application. Please contact support if you believe this is an error.".to_owned())]
105    pub banned_user_message: String,
106    /// Optional asynchronous message callback over the stored application user.
107    /// When configured, this takes precedence over `banned_user_message`.
108    #[config(skip, default = None)]
109    pub banned_user_message_callback: Option<AdminBannedUserMessageHandler>,
110    /// Whether other admin users may be impersonated.
111    #[config(default = false)]
112    pub allow_impersonating_admins: bool,
113}
114
115alibi_core::impl_auth_plugin! {
116    AdminPlugin, "admin";
117    routes {
118        post "/admin/set-role" => handle_set_role, "admin_set_role";
119        get  "/admin/get-user" => handle_get_user, "admin_get_user";
120        post "/admin/create-user" => handle_create_user, "admin_create_user";
121        post "/admin/update-user" => handle_update_user, "admin_update_user";
122        get  "/admin/list-users" => handle_list_users, "admin_list_users";
123        post "/admin/list-user-sessions" => handle_list_user_sessions, "admin_list_user_sessions";
124        post "/admin/ban-user" => handle_ban_user, "admin_ban_user";
125        post "/admin/unban-user" => handle_unban_user, "admin_unban_user";
126        post "/admin/impersonate-user" => handle_impersonate_user, "admin_impersonate_user";
127        post "/admin/stop-impersonating" => handle_stop_impersonating, "admin_stop_impersonating";
128        post "/admin/revoke-user-session" => handle_revoke_user_session, "admin_revoke_user_session";
129        post "/admin/revoke-user-sessions" => handle_revoke_user_sessions, "admin_revoke_user_sessions";
130        post "/admin/remove-user" => handle_remove_user, "admin_remove_user";
131        post "/admin/set-user-password" => handle_set_user_password, "admin_set_user_password";
132        post "/admin/has-permission" => handle_has_permission, "admin_has_permission";
133    }
134    extra {
135    route_openapi_metadata!(S);
136
137        fn session_fields(&self) -> alibi_core::field_policy::FieldConfigs {
138            std::iter::once(("impersonatedBy".into(), alibi_core::field_policy::FieldConfig::new(serde_json::json!({"type":"string"})).read_only())).collect()
139        }
140
141        async fn on_init(
142            &self,
143            ctx: &mut alibi_core::AuthInitContext<S>,
144        ) -> AuthResult<()> {
145            if let Some(admin_roles) = &self.config.admin_roles {
146                let roles = self.config.roles.clone().unwrap_or_else(access::default_roles);
147                let names: Vec<_> = roles.keys().map(|name| name.to_lowercase()).collect();
148                let invalid: Vec<_> = admin_roles.iter()
149                    .filter(|role| !names.contains(&role.to_lowercase()))
150                    .map(String::as_str).collect();
151                if !invalid.is_empty() {
152                    return Err(AuthError::config(format!(
153                        "Invalid admin roles: {}. Admin roles must be defined in the 'roles' configuration.",
154                        invalid.join(", ")
155                    )));
156                }
157            }
158            if let Some(handler) = &self.config.banned_user_message_callback {
159                handler.validate::<S::User>()?;
160                ctx.extensions.insert(BannedUserMessagePolicy(handler.clone()));
161            }
162            let default_role = self.config.default_role.clone();
163            let validation_enabled = ctx.config.user_validation.is_some();
164            ctx.register_user_create_transform(move |mut input| {
165                if !validation_enabled {_ = input.banned.get_or_insert(false);}
166                _ = input.role.get_or_insert_with(|| default_role.clone());
167                Ok(input)
168            });
169            if validation_enabled {ctx.register_user_creation_adapter_default(|mut input| {
170                _ = input.banned.get_or_insert(false); Ok(input)
171            });}
172            ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
173            ctx.set_metadata(
174                "admin.default_role",
175                serde_json::Value::String(self.config.default_role.clone()),
176            );
177            ctx.set_metadata(
178                "admin.banned_user_message",
179                serde_json::Value::String(self.config.banned_user_message.clone()),
180            );
181            Ok(())
182        }
183    }
184}
185
186impl AdminPlugin {
187    /// Configure an awaited message callback over the exact stored user entity.
188    #[must_use]
189    pub fn banned_user_message_callback<U: AuthUser, H: AdminBannedUserMessage<U>>(
190        mut self,
191        handler: H,
192    ) -> Self {
193        self.config.banned_user_message_callback =
194            Some(AdminBannedUserMessageHandler::new::<U, H>(handler));
195        self
196    }
197
198    async fn require_session(
199        &self,
200        req: &AuthRequest,
201        ctx: &AuthContext<impl alibi_core::AuthSchema>,
202    ) -> AuthResult<Option<(UserView, SessionView)>> {
203        match ctx.require_authoritative_cached_session(req).await {
204            Ok((user, session)) => Ok(Some((UserView::from(&user), SessionView::from(&session)))),
205            Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => Ok(None),
206            Err(error) => Err(error),
207        }
208    }
209
210    fn missing_session_response() -> AuthResponse {
211        AuthResponse::new(401).with_header("Content-Type", "application/json")
212    }
213
214    fn authorize(
215        &self,
216        user: &UserView,
217        resource: &str,
218        action: &str,
219        message: &str,
220    ) -> AuthResult<()> {
221        let permissions = HashMap::from([(resource.to_owned(), vec![action.to_owned()])]);
222        if has_permission(
223            Some(user.id.as_str()),
224            user.role.as_deref(),
225            &self.config,
226            &permissions,
227        ) {
228            Ok(())
229        } else {
230            Err(AuthError::forbidden(message))
231        }
232    }
233
234    async fn handle_set_role(
235        &self,
236        req: &AuthRequest,
237        ctx: &AuthContext<impl alibi_core::AuthSchema>,
238    ) -> AuthResult<AuthResponse> {
239        let body: SetRoleRequest = match validation::body(req) {
240            Ok(v) => v,
241            Err(resp) => return Ok(resp),
242        };
243        let Some((user, _session)) = self.require_session(req, ctx).await? else {
244            return Ok(Self::missing_session_response());
245        };
246        self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
247        let response = set_role_core(&body, &self.config, ctx).await?;
248        AuthResponse::json(200, &response).map_err(AuthError::from)
249    }
250
251    async fn handle_get_user(
252        &self,
253        req: &AuthRequest,
254        ctx: &AuthContext<impl alibi_core::AuthSchema>,
255    ) -> AuthResult<AuthResponse> {
256        let query = match validation::get_user(req) {
257            Ok(query) => query,
258            Err(response) => return Ok(response),
259        };
260        let Some((user, _session)) = self.require_session(req, ctx).await? else {
261            return Ok(Self::missing_session_response());
262        };
263        self.authorize(&user, "user", "get", MESSAGE_GET_USER)?;
264        let response = get_user_core(&query, ctx).await?;
265        AuthResponse::json(200, &response).map_err(AuthError::from)
266    }
267
268    async fn handle_create_user(
269        &self,
270        req: &AuthRequest,
271        ctx: &AuthContext<impl alibi_core::AuthSchema>,
272    ) -> AuthResult<AuthResponse> {
273        let body: CreateUserRequest = match validation::body(req) {
274            Ok(v) => v,
275            Err(resp) => return Ok(resp),
276        };
277        let Some((user, _session)) = self.require_session(req, ctx).await? else {
278            return Ok(Self::missing_session_response());
279        };
280        self.authorize(&user, "user", "create", MESSAGE_CREATE_USERS)?;
281        // The source checks presence after nullish body/data precedence; empty
282        // strings/arrays still request role authority. Validate nested types later.
283        if body.role.is_some()
284            || body
285                .data
286                .as_ref()
287                .is_some_and(|data| data.contains_key("role"))
288        {
289            self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
290        }
291        if body.data.as_ref().is_some_and(|data| {
292            ["banned", "banReason", "banExpires"]
293                .iter()
294                .any(|key| data.contains_key(*key))
295        }) {
296            self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
297        }
298        let response = create_user_core(&body, &self.config, ctx).await?;
299        AuthResponse::json(200, &response).map_err(AuthError::from)
300    }
301
302    async fn handle_update_user(
303        &self,
304        req: &AuthRequest,
305        ctx: &AuthContext<impl alibi_core::AuthSchema>,
306    ) -> AuthResult<AuthResponse> {
307        let mut body: AdminUpdateUserRequest = match validation::body(req) {
308            Ok(v) => v,
309            Err(resp) => return Ok(resp),
310        };
311        let Some((user, _session)) = self.require_session(req, ctx).await? else {
312            return Ok(Self::missing_session_response());
313        };
314        self.authorize(&user, "user", "update", MESSAGE_UPDATE_USERS)?;
315        let username = body
316            .data
317            .remove("username")
318            .and_then(|value| value.as_str().map(ToOwned::to_owned))
319            .map(|value| value.to_lowercase());
320        let display_username = body
321            .data
322            .remove("displayUsername")
323            .and_then(|value| value.as_str().map(ToOwned::to_owned));
324
325        if let Some(username) = username.as_deref() {
326            match validate_username(username) {
327                Ok(()) => {}
328                Err(UsernameValidationError::TooShort) => {
329                    return username_error_response(
330                        400,
331                        "USERNAME_TOO_SHORT",
332                        MESSAGE_USERNAME_TOO_SHORT,
333                    );
334                }
335                Err(UsernameValidationError::TooLong) => {
336                    return username_error_response(
337                        400,
338                        "USERNAME_IS_TOO_LONG",
339                        MESSAGE_USERNAME_TOO_LONG,
340                    );
341                }
342                Err(UsernameValidationError::Invalid) => {
343                    return username_error_response(
344                        400,
345                        "USERNAME_IS_INVALID",
346                        MESSAGE_INVALID_USERNAME,
347                    );
348                }
349            }
350
351            if let Some(existing_user) = ctx.database.get_user_by_username(username).await?
352                && AuthUser::id(&existing_user).as_ref() != body.user_id
353            {
354                return username_error_response(
355                    400,
356                    "USERNAME_IS_ALREADY_TAKEN",
357                    MESSAGE_USERNAME_IS_ALREADY_TAKEN,
358                );
359            }
360        }
361
362        if let Some(username) = username {
363            _ = body
364                .data
365                .insert("username".to_owned(), serde_json::Value::String(username));
366        }
367        if let Some(display_username) = display_username {
368            _ = body.data.insert(
369                "displayUsername".to_owned(),
370                serde_json::Value::String(display_username),
371            );
372        }
373
374        let response = update_user_core(&body, &user, &self.config, ctx).await?;
375        AuthResponse::json(200, &response).map_err(AuthError::from)
376    }
377
378    async fn handle_list_users(
379        &self,
380        req: &AuthRequest,
381        ctx: &AuthContext<impl alibi_core::AuthSchema>,
382    ) -> AuthResult<AuthResponse> {
383        if let Err(response) = validation::list_users(req) {
384            return Ok(response);
385        }
386        let Some((user, _session)) = self.require_session(req, ctx).await? else {
387            return Ok(Self::missing_session_response());
388        };
389        self.authorize(&user, "user", "list", MESSAGE_LIST_USERS)?;
390        let query = ListUsersQueryParams {
391            limit: req.query.get("limit").and_then(|value| value.parse().ok()),
392            offset: req.query.get("offset").and_then(|value| value.parse().ok()),
393            search_field: req.query.get("searchField").cloned(),
394            search_value: req.query.get("searchValue").cloned(),
395            search_operator: req.query.get("searchOperator").cloned(),
396            sort_by: req.query.get("sortBy").cloned(),
397            sort_direction: req.query.get("sortDirection").cloned(),
398            filter_field: req.query.get("filterField").cloned(),
399            filter_value: req.query_values("filterValue").map(|values| match values {
400                [value] => alibi_core::UserFilterValue::Scalar(value.clone()),
401                values => alibi_core::UserFilterValue::Multiple(values.to_vec()),
402            }),
403            filter_operator: req.query.get("filterOperator").cloned(),
404        };
405        let response = list_users_core(&query, ctx).await?;
406        AuthResponse::json(200, &response).map_err(AuthError::from)
407    }
408
409    async fn handle_list_user_sessions(
410        &self,
411        req: &AuthRequest,
412        ctx: &AuthContext<impl alibi_core::AuthSchema>,
413    ) -> AuthResult<AuthResponse> {
414        let body: UserIdRequest = match validation::body(req) {
415            Ok(v) => v,
416            Err(resp) => return Ok(resp),
417        };
418        let Some((user, _session)) = self.require_session(req, ctx).await? else {
419            return Ok(Self::missing_session_response());
420        };
421        self.authorize(&user, "session", "list", MESSAGE_LIST_USER_SESSIONS)?;
422        let response = list_user_sessions_core(&body, ctx).await?;
423        AuthResponse::json(200, &response).map_err(AuthError::from)
424    }
425
426    async fn handle_ban_user(
427        &self,
428        req: &AuthRequest,
429        ctx: &AuthContext<impl alibi_core::AuthSchema>,
430    ) -> AuthResult<AuthResponse> {
431        let body: BanUserRequest = match validation::body(req) {
432            Ok(v) => v,
433            Err(resp) => return Ok(resp),
434        };
435        let Some((user, _session)) = self.require_session(req, ctx).await? else {
436            return Ok(Self::missing_session_response());
437        };
438        self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
439        let response = match ban_user_core(&body, user.id.as_str(), &self.config, ctx).await {
440            Ok(response) => response,
441            Err(AdminDateOperationError::InvalidDate) => return Ok(AuthResponse::new(500)),
442            Err(AdminDateOperationError::Auth(error)) => return Err(error),
443        };
444        AuthResponse::json(200, &response).map_err(AuthError::from)
445    }
446
447    async fn handle_unban_user(
448        &self,
449        req: &AuthRequest,
450        ctx: &AuthContext<impl alibi_core::AuthSchema>,
451    ) -> AuthResult<AuthResponse> {
452        let body: UserIdRequest = match validation::body(req) {
453            Ok(v) => v,
454            Err(resp) => return Ok(resp),
455        };
456        let Some((user, _session)) = self.require_session(req, ctx).await? else {
457            return Ok(Self::missing_session_response());
458        };
459        self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
460        let response = unban_user_core(&body, ctx).await?;
461        AuthResponse::json(200, &response).map_err(AuthError::from)
462    }
463
464    async fn handle_impersonate_user(
465        &self,
466        req: &AuthRequest,
467        ctx: &AuthContext<impl alibi_core::AuthSchema>,
468    ) -> AuthResult<AuthResponse> {
469        let body: UserIdRequest = match validation::body(req) {
470            Ok(v) => v,
471            Err(resp) => return Ok(resp),
472        };
473        let Some((user, session)) = self.require_session(req, ctx).await? else {
474            return Ok(Self::missing_session_response());
475        };
476        self.authorize(&user, "user", "impersonate", MESSAGE_IMPERSONATE_USERS)?;
477        let metadata = alibi_core::RequestMeta::from_request(req);
478        let (response, token) = match impersonate_user_core(
479            &body,
480            user.id.as_str(),
481            user.role.as_deref(),
482            metadata.ip_address.as_deref(),
483            metadata.user_agent.as_deref(),
484            &self.config,
485            ctx,
486        )
487        .await
488        {
489            Ok(response) => response,
490            Err(AdminDateOperationError::InvalidDate) => return Ok(AuthResponse::new(500)),
491            Err(AdminDateOperationError::Auth(error)) => return Err(error),
492        };
493        let dont_remember = get_cookie(req, &related_cookie_name(&ctx.config, "dont_remember"))
494            .and_then(|value| {
495                alibi_core::utils::cookie_utils::verify_cookie_value(
496                    &value,
497                    ctx.config.current_secret(),
498                )
499            })
500            .is_some_and(|value| !value.is_empty());
501        let admin_cookie = create_admin_session_cookie_value(
502            ctx.config.current_secret(),
503            &AdminSessionCookiePayload {
504                session_token: session.token.clone(),
505                dont_remember,
506            },
507            ctx.config.session.expires_in,
508        )?;
509        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
510
511        let mut auth_response = AuthResponse::json(200, &response)?;
512        for cookie in delete_session_cookie_headers(&ctx.config)? {
513            auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
514        }
515        auth_response = auth_response.with_appended_header(
516            "Set-Cookie",
517            create_session_like_cookie(
518                &admin_cookie_name,
519                &admin_cookie,
520                Some(ctx.config.session.expires_in.num_seconds()),
521                &ctx.config,
522            )?,
523        );
524        auth_response = auth_response.with_appended_header(
525            "Set-Cookie",
526            create_session_cookie_with_max_age(Some(&token), None, &ctx.config)?,
527        );
528        auth_response = auth_response.with_appended_header(
529            "Set-Cookie",
530            create_session_like_cookie(
531                &related_cookie_name(&ctx.config, "dont_remember"),
532                &alibi_core::utils::cookie_utils::sign_cookie_value(
533                    "true",
534                    ctx.config.current_secret(),
535                ),
536                None,
537                &ctx.config,
538            )?,
539        );
540        Ok(auth_response)
541    }
542
543    async fn handle_stop_impersonating(
544        &self,
545        req: &AuthRequest,
546        ctx: &AuthContext<impl alibi_core::AuthSchema>,
547    ) -> AuthResult<AuthResponse> {
548        if let Err(response) = validation::parse(req) {
549            return Ok(response);
550        }
551        let session = match ctx.require_cached_session(req).await {
552            Ok((_, session)) => session,
553            Err(AuthError::Unauthenticated) => return Ok(Self::missing_session_response()),
554            Err(error) => return Err(error),
555        };
556        if session.impersonated_by.is_none() {
557            return Err(AuthError::bad_request("You are not impersonating anyone"));
558        }
559
560        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
561        let admin_cookie_value =
562            get_cookie(req, &admin_cookie_name).ok_or_else(|| AuthError::Api {
563                status: 500,
564                code: None,
565                message: "Failed to find admin session".into(),
566            })?;
567        let admin_cookie =
568            decode_admin_session_cookie_value(ctx.config.current_secret(), &admin_cookie_value)
569                .map_err(|_error| AuthError::Api {
570                    status: 500,
571                    code: None,
572                    message: "Failed to find admin session".into(),
573                })?;
574
575        let (response, new_token) = stop_impersonating_core(&session, &admin_cookie, ctx).await?;
576
577        let mut auth_response = AuthResponse::json(200, &response)?;
578        auth_response = auth_response.with_appended_header(
579            "Set-Cookie",
580            create_session_cookie_with_max_age(
581                Some(&new_token),
582                if admin_cookie.dont_remember {
583                    None
584                } else {
585                    Some(ctx.config.session.expires_in.num_seconds())
586                },
587                &ctx.config,
588            )?,
589        );
590        if admin_cookie.dont_remember {
591            auth_response = auth_response.with_appended_header(
592                "Set-Cookie",
593                create_session_like_cookie(
594                    &related_cookie_name(&ctx.config, "dont_remember"),
595                    &alibi_core::utils::cookie_utils::sign_cookie_value(
596                        "true",
597                        ctx.config.current_secret(),
598                    ),
599                    None,
600                    &ctx.config,
601                )?,
602            );
603        }
604        auth_response = auth_response.with_appended_header(
605            "Set-Cookie",
606            create_clear_cookie(&admin_cookie_name, &ctx.config)?,
607        );
608        Ok(auth_response)
609    }
610
611    async fn handle_revoke_user_session(
612        &self,
613        req: &AuthRequest,
614        ctx: &AuthContext<impl alibi_core::AuthSchema>,
615    ) -> AuthResult<AuthResponse> {
616        let body: RevokeSessionRequest = match validation::body(req) {
617            Ok(v) => v,
618            Err(resp) => return Ok(resp),
619        };
620        let Some((user, _session)) = self.require_session(req, ctx).await? else {
621            return Ok(Self::missing_session_response());
622        };
623        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
624        let response = revoke_user_session_core(&body, ctx).await?;
625        AuthResponse::json(200, &response).map_err(AuthError::from)
626    }
627
628    async fn handle_revoke_user_sessions(
629        &self,
630        req: &AuthRequest,
631        ctx: &AuthContext<impl alibi_core::AuthSchema>,
632    ) -> AuthResult<AuthResponse> {
633        let body: UserIdRequest = match validation::body(req) {
634            Ok(v) => v,
635            Err(resp) => return Ok(resp),
636        };
637        let Some((user, _session)) = self.require_session(req, ctx).await? else {
638            return Ok(Self::missing_session_response());
639        };
640        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
641        let response = revoke_user_sessions_core(&body, ctx).await?;
642        AuthResponse::json(200, &response).map_err(AuthError::from)
643    }
644
645    async fn handle_remove_user(
646        &self,
647        req: &AuthRequest,
648        ctx: &AuthContext<impl alibi_core::AuthSchema>,
649    ) -> AuthResult<AuthResponse> {
650        let body: UserIdRequest = match validation::body(req) {
651            Ok(v) => v,
652            Err(resp) => return Ok(resp),
653        };
654        let Some((user, _session)) = self.require_session(req, ctx).await? else {
655            return Ok(Self::missing_session_response());
656        };
657        self.authorize(&user, "user", "delete", MESSAGE_DELETE_USERS)?;
658        let response = remove_user_core(&body, user.id.as_str(), ctx).await?;
659        AuthResponse::json(200, &response).map_err(AuthError::from)
660    }
661
662    async fn handle_set_user_password(
663        &self,
664        req: &AuthRequest,
665        ctx: &AuthContext<impl alibi_core::AuthSchema>,
666    ) -> AuthResult<AuthResponse> {
667        let body: SetUserPasswordRequest = match validation::body(req) {
668            Ok(v) => v,
669            Err(resp) => return Ok(resp),
670        };
671        let Some((user, _session)) = self.require_session(req, ctx).await? else {
672            return Ok(Self::missing_session_response());
673        };
674        self.authorize(&user, "user", "set-password", MESSAGE_SET_USER_PASSWORD)?;
675        let response = set_user_password_core(&body, ctx).await?;
676        AuthResponse::json(200, &response).map_err(AuthError::from)
677    }
678
679    async fn handle_has_permission(
680        &self,
681        req: &AuthRequest,
682        ctx: &AuthContext<impl alibi_core::AuthSchema>,
683    ) -> AuthResult<AuthResponse> {
684        let body: HasPermissionRequest = match validation::body(req) {
685            Ok(v) => v,
686            Err(resp) => return Ok(resp),
687        };
688        if body.permissions.is_none() {
689            return AuthResponse::json(400, &serde_json::json!({"message":"invalid permission check. no permission(s) were passed."})).map_err(AuthError::from);
690        }
691        let Some((user, _session)) = self.require_session(req, ctx).await? else {
692            return Ok(Self::missing_session_response());
693        };
694        let response = has_permission_core(&body, &user, &self.config)?;
695        AuthResponse::json(200, &response).map_err(AuthError::from)
696    }
697}
698
699impl std::fmt::Debug for AdminPlugin {
700    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701        f.debug_struct("AdminPlugin").finish_non_exhaustive()
702    }
703}
704
705fn username_error_response(status: u16, code: &str, message: &str) -> AuthResult<AuthResponse> {
706    AuthResponse::json(
707        status,
708        &ErrorCodeMessageResponse {
709            code: Some(code.to_owned()),
710            message: message.to_owned(),
711        },
712    )
713    .map_err(AuthError::from)
714}
715
716pub(super) fn target_is_admin(
717    user_id: Option<&str>,
718    role: Option<&str>,
719    config: &AdminConfig,
720) -> bool {
721    is_admin_user_id(user_id, config) || is_admin_role(role, config)
722}
723
724// LCOV_EXCL_START
725#[cfg(test)]
726mod tests {
727    use super::*;
728    use crate::test_helpers;
729    use alibi_core::entity::{AuthAccount, AuthSession};
730    use alibi_core::utils::cookie_utils::related_cookie_name;
731    use alibi_core::wire::{SessionView, UserView};
732    use alibi_core::{AuthPlugin, CreateSession, CreateUser, HttpMethod};
733    use chrono::{Duration, Utc};
734    use std::collections::HashMap;
735    use std::sync::Arc;
736
737    type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
738
739    async fn create_admin_context() -> (
740        AuthContext<TestSchema>,
741        UserView,
742        SessionView,
743        UserView,
744        SessionView,
745    ) {
746        let ctx = test_helpers::create_test_context().await;
747
748        let admin = test_helpers::create_user(
749            &ctx,
750            CreateUser::new()
751                .with_email("admin@example.com")
752                .with_name("Admin")
753                .with_role("admin"),
754        )
755        .await;
756        let admin_session =
757            test_helpers::create_session(&ctx, admin.id.clone(), Duration::hours(24)).await;
758
759        let user = test_helpers::create_user(
760            &ctx,
761            CreateUser::new()
762                .with_email("user@example.com")
763                .with_name("Regular User")
764                .with_role("user"),
765        )
766        .await;
767        let user_session =
768            test_helpers::create_session(&ctx, user.id.clone(), Duration::hours(24)).await;
769
770        (ctx, admin, admin_session, user, user_session)
771    }
772
773    fn make_request(
774        method: HttpMethod,
775        path: &str,
776        token: &str,
777        body: Option<serde_json::Value>,
778    ) -> AuthRequest {
779        test_helpers::create_auth_json_request_no_query(method, path, Some(token), body)
780    }
781
782    fn json_body(resp: &AuthResponse) -> serde_json::Value {
783        serde_json::from_slice(&resp.body).unwrap()
784    }
785
786    fn set_cookie_value(resp: &AuthResponse, name: &str) -> Option<String> {
787        resp.headers.get_all("Set-Cookie").find_map(|header| {
788            let (cookie_name, remainder) = header.split_once('=')?;
789            if cookie_name != name {
790                return None;
791            }
792            Some(remainder.split(';').next().unwrap_or_default().to_owned())
793        })
794    }
795
796    #[tokio::test]
797    async fn test_custom_admin_role_can_use_permission_engine() {
798        let config = Arc::new(alibi_core::AuthConfig::new(
799            "test-secret-key-at-least-32-chars-long",
800        ));
801        let database = test_helpers::create_test_database().await;
802        let ctx = AuthContext::new(config, Arc::clone(&database));
803
804        let admin = database
805            .create_user(
806                CreateUser::new()
807                    .with_email("superadmin@example.com")
808                    .with_name("Super Admin")
809                    .with_role("superadmin"),
810            )
811            .await
812            .unwrap();
813
814        let admin_session = database
815            .create_session(CreateSession {
816                additional_fields: alibi_core::field_policy::FieldValues::default(),
817                token: None,
818                active_team_id: None,
819                user_id: admin.id.clone(),
820                expires_at: Utc::now() + Duration::hours(24),
821                ip_address: None,
822                user_agent: None,
823                impersonated_by: None,
824                active_organization_id: None,
825            })
826            .await
827            .unwrap();
828
829        _ = database
830            .create_user(
831                CreateUser::new()
832                    .with_email("user@example.com")
833                    .with_name("User")
834                    .with_role("user"),
835            )
836            .await
837            .unwrap();
838
839        let plugin = AdminPlugin::with_config(AdminConfig {
840            admin_roles: Some(vec!["superadmin".to_owned()]),
841            roles: Some(HashMap::from([(
842                "superadmin".to_owned(),
843                RolePermissions::new()
844                    .allow(
845                        "user",
846                        [
847                            "create",
848                            "list",
849                            "set-role",
850                            "ban",
851                            "impersonate",
852                            "delete",
853                            "set-password",
854                            "get",
855                            "update",
856                        ],
857                    )
858                    .allow("session", ["list", "revoke", "delete"]),
859            )])),
860            ..Default::default()
861        });
862
863        let req = make_request(
864            HttpMethod::Get,
865            "/admin/list-users",
866            &admin_session.token,
867            None,
868        );
869
870        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
871        assert_eq!(resp.status, 200);
872        assert_eq!(
873            (*(json_body(&resp))
874                .get("total")
875                .unwrap_or(&serde_json::Value::Null)),
876            2
877        );
878    }
879
880    #[tokio::test]
881    async fn test_ban_revokes_user_sessions() {
882        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
883        let plugin = AdminPlugin::new();
884
885        let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
886        assert_ne!(sessions.len(), 0);
887
888        let req = make_request(
889            HttpMethod::Post,
890            "/admin/ban-user",
891            &admin_session.token,
892            Some(serde_json::json!({
893                "userId": user.id,
894                "banReason": "bad behavior"
895            })),
896        );
897
898        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
899        assert_eq!(resp.status, 200);
900
901        let sessions_2 = ctx.database.get_user_sessions(&user.id).await.unwrap();
902        assert_eq!(sessions_2.len(), 0);
903    }
904
905    #[tokio::test]
906    async fn test_unban_clears_ban_reason_and_expires() {
907        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
908        let plugin = AdminPlugin::new();
909
910        let req = make_request(
911            HttpMethod::Post,
912            "/admin/ban-user",
913            &admin_session.token,
914            Some(serde_json::json!({
915                "userId": user.id,
916                "banReason": "spam",
917                "banExpiresIn": 3600
918            })),
919        );
920        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
921        assert_eq!(resp.status, 200);
922
923        let req_2 = make_request(
924            HttpMethod::Post,
925            "/admin/unban-user",
926            &admin_session.token,
927            Some(serde_json::json!({
928                "userId": user.id,
929            })),
930        );
931
932        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
933        assert_eq!(resp_2.status, 200);
934
935        let updated_user = ctx
936            .database
937            .get_user_by_id(&user.id)
938            .await
939            .unwrap()
940            .unwrap();
941        assert_eq!(updated_user.banned, Some(false));
942        assert!(updated_user.ban_reason.is_none());
943        assert!(updated_user.ban_expires.is_none());
944    }
945
946    #[tokio::test]
947    async fn test_impersonation_session_tracks_admin_id() {
948        let (ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
949        let plugin = AdminPlugin::new();
950
951        let req = make_request(
952            HttpMethod::Post,
953            "/admin/impersonate-user",
954            &admin_session.token,
955            Some(serde_json::json!({
956                "userId": user.id,
957            })),
958        );
959
960        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
961        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
962        assert!(
963            set_cookie_value(&resp, &admin_cookie_name).is_some(),
964            "impersonation should emit an admin_session cookie"
965        );
966        let token = (*(*(json_body(&resp))
967            .get("session")
968            .unwrap_or(&serde_json::Value::Null))
969        .get("token")
970        .unwrap_or(&serde_json::Value::Null))
971        .as_str()
972        .unwrap()
973        .to_owned();
974        let session = ctx.database.get_session(&token).await.unwrap().unwrap();
975
976        assert_eq!(session.impersonated_by().unwrap(), admin.id);
977    }
978
979    #[tokio::test]
980    async fn test_stop_impersonating_restores_admin_session() {
981        let (mut ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
982        let plugin = AdminPlugin::new();
983        let mut init =
984            alibi_core::AuthInitContext::new(Arc::clone(&ctx.config), Arc::clone(&ctx.database));
985        plugin.on_init(&mut init).await.unwrap();
986        ctx.metadata.extend(init.into_parts().metadata);
987
988        let req = make_request(
989            HttpMethod::Post,
990            "/admin/impersonate-user",
991            &admin_session.token,
992            Some(serde_json::json!({
993                "userId": user.id,
994            })),
995        );
996        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
997        let impersonation_token = (*(*(json_body(&resp))
998            .get("session")
999            .unwrap_or(&serde_json::Value::Null))
1000        .get("token")
1001        .unwrap_or(&serde_json::Value::Null))
1002        .as_str()
1003        .unwrap()
1004        .to_owned();
1005        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
1006        let admin_cookie = set_cookie_value(&resp, &admin_cookie_name)
1007            .expect("impersonation should set the admin_session cookie");
1008
1009        let mut req_2 = make_request(
1010            HttpMethod::Post,
1011            "/admin/stop-impersonating",
1012            &impersonation_token,
1013            None,
1014        );
1015        req_2.headers.insert(
1016            "cookie".to_owned(),
1017            format!(
1018                "{}; {admin_cookie_name}={admin_cookie}",
1019                req_2
1020                    .headers
1021                    .get("cookie")
1022                    .expect("impersonated browser has a signed session cookie")
1023            ),
1024        );
1025        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1026        let body = json_body(&resp_2);
1027
1028        let restored_token = (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1029            .get("token")
1030            .unwrap_or(&serde_json::Value::Null))
1031        .as_str()
1032        .unwrap();
1033        assert_eq!(
1034            restored_token, admin_session.token,
1035            "stop-impersonating should restore the original admin session token"
1036        );
1037        let restored_session = ctx
1038            .database
1039            .get_session(restored_token)
1040            .await
1041            .unwrap()
1042            .unwrap();
1043        assert_eq!(restored_session.user_id, admin.id);
1044        assert!(restored_session.impersonated_by.is_none());
1045        assert!(
1046            ctx.database
1047                .get_session(&impersonation_token)
1048                .await
1049                .unwrap()
1050                .is_none()
1051        );
1052        let cleared_admin_cookie = resp_2
1053            .headers
1054            .get_all("Set-Cookie")
1055            .find(|header| header.starts_with(&format!("{admin_cookie_name}=")))
1056            .expect("stop-impersonating should clear admin_session");
1057        assert!(
1058            cleared_admin_cookie.contains("Max-Age=0"),
1059            "admin_session should be cleared after stop-impersonating"
1060        );
1061    }
1062
1063    #[tokio::test]
1064    async fn test_list_user_sessions_missing_user_returns_empty_array() {
1065        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1066        let plugin = AdminPlugin::new();
1067
1068        let req = make_request(
1069            HttpMethod::Post,
1070            "/admin/list-user-sessions",
1071            &admin_session.token,
1072            Some(serde_json::json!({
1073                "userId": "missing-user",
1074            })),
1075        );
1076        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1077
1078        assert_eq!(resp.status, 200);
1079        assert_eq!(json_body(&resp), serde_json::json!({ "sessions": [] }));
1080    }
1081
1082    #[tokio::test]
1083    async fn test_revoke_user_sessions_missing_user_still_succeeds() {
1084        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1085        let plugin = AdminPlugin::new();
1086
1087        let req = make_request(
1088            HttpMethod::Post,
1089            "/admin/revoke-user-sessions",
1090            &admin_session.token,
1091            Some(serde_json::json!({
1092                "userId": "missing-user",
1093            })),
1094        );
1095        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1096
1097        assert_eq!(resp.status, 200);
1098        assert_eq!(json_body(&resp), serde_json::json!({ "success": true }));
1099    }
1100
1101    #[tokio::test]
1102    async fn test_stop_impersonating_without_impersonated_session_returns_bad_request() {
1103        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1104        let plugin = AdminPlugin::new();
1105
1106        let req = make_request(
1107            HttpMethod::Post,
1108            "/admin/stop-impersonating",
1109            &admin_session.token,
1110            None,
1111        );
1112        let err = plugin.on_request(&req, &ctx).await.unwrap_err();
1113        let response = err.to_auth_response();
1114        assert_eq!(response.status, 400);
1115        assert_eq!(
1116            json_body(&response),
1117            serde_json::json!({
1118                // Upstream throws this via `APIError.fromStatus`, which carries no
1119                // code — only errors built from a code constant have one.
1120                "message": "You are not impersonating anyone"
1121            })
1122        );
1123    }
1124
1125    #[tokio::test]
1126    async fn test_remove_user_cleans_up_sessions_and_accounts() {
1127        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1128        let plugin = AdminPlugin::new();
1129
1130        let req = make_request(
1131            HttpMethod::Post,
1132            "/admin/create-user",
1133            &admin_session.token,
1134            Some(serde_json::json!({
1135                "email": "tobedeleted@example.com",
1136                "password": "securepassword123",
1137                "name": "To Be Deleted"
1138            })),
1139        );
1140        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1141        let user_id = (*(*(json_body(&resp))
1142            .get("user")
1143            .unwrap_or(&serde_json::Value::Null))
1144        .get("id")
1145        .unwrap_or(&serde_json::Value::Null))
1146        .as_str()
1147        .unwrap()
1148        .to_owned();
1149
1150        let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
1151        assert_eq!(accounts.len(), 1);
1152
1153        let req_2 = make_request(
1154            HttpMethod::Post,
1155            "/admin/remove-user",
1156            &admin_session.token,
1157            Some(serde_json::json!({
1158                "userId": user_id,
1159            })),
1160        );
1161        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1162        assert_eq!(resp_2.status, 200);
1163
1164        assert!(
1165            ctx.database
1166                .get_user_by_id(&user_id)
1167                .await
1168                .unwrap()
1169                .is_none()
1170        );
1171        assert_eq!(
1172            ctx.database
1173                .get_user_accounts(&user_id)
1174                .await
1175                .unwrap()
1176                .len(),
1177            0
1178        );
1179    }
1180
1181    #[tokio::test]
1182    async fn test_set_user_password_updates_credential_account() {
1183        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1184        let plugin = AdminPlugin::new();
1185
1186        let req = make_request(
1187            HttpMethod::Post,
1188            "/admin/create-user",
1189            &admin_session.token,
1190            Some(serde_json::json!({
1191                "email": "pwuser@example.com",
1192                "password": "oldpassword123",
1193                "name": "PW User"
1194            })),
1195        );
1196        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1197        let user_id = (*(*(json_body(&resp))
1198            .get("user")
1199            .unwrap_or(&serde_json::Value::Null))
1200        .get("id")
1201        .unwrap_or(&serde_json::Value::Null))
1202        .as_str()
1203        .unwrap()
1204        .to_owned();
1205
1206        let before = ctx.database.get_user_accounts(&user_id).await.unwrap();
1207        let old_password = (*(before)
1208            .first()
1209            .expect("persisted rows contain the requested index"))
1210        .password()
1211        .unwrap()
1212        .to_owned();
1213
1214        let req_2 = make_request(
1215            HttpMethod::Post,
1216            "/admin/set-user-password",
1217            &admin_session.token,
1218            Some(serde_json::json!({
1219                "userId": user_id,
1220                "newPassword": "newpassword456"
1221            })),
1222        );
1223        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1224        assert_eq!(resp_2.status, 200);
1225
1226        let after = ctx.database.get_user_accounts(&user_id).await.unwrap();
1227        let new_password = (*(after)
1228            .first()
1229            .expect("persisted rows contain the requested index"))
1230        .password()
1231        .unwrap()
1232        .to_owned();
1233        assert_ne!(old_password, new_password);
1234    }
1235
1236    #[tokio::test]
1237    async fn test_set_user_password_creates_canonical_credential_when_missing() {
1238        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1239        let plugin = AdminPlugin::new();
1240
1241        let req = make_request(
1242            HttpMethod::Post,
1243            "/admin/create-user",
1244            &admin_session.token,
1245            Some(serde_json::json!({
1246                "email": "passwordless@example.com",
1247                "name": "Passwordless User"
1248            })),
1249        );
1250        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1251        let user_id = (*(*(json_body(&resp))
1252            .get("user")
1253            .unwrap_or(&serde_json::Value::Null))
1254        .get("id")
1255        .unwrap_or(&serde_json::Value::Null))
1256        .as_str()
1257        .unwrap()
1258        .to_owned();
1259
1260        let req_2 = make_request(
1261            HttpMethod::Post,
1262            "/admin/set-user-password",
1263            &admin_session.token,
1264            Some(serde_json::json!({
1265                "userId": user_id,
1266                "newPassword": "newpassword456"
1267            })),
1268        );
1269        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1270        assert_eq!(resp_2.status, 200);
1271
1272        let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
1273        assert_eq!(accounts.len(), 1);
1274        let account = accounts
1275            .first()
1276            .expect("password setting creates a credential");
1277        assert_eq!(account.user_id(), user_id);
1278        assert_eq!(account.account_id(), user_id);
1279        assert_eq!(account.provider_id(), "credential");
1280        assert!(
1281            alibi_core::PasswordHasher::verify(
1282                &alibi_core::ScryptHasher,
1283                account.password().unwrap(),
1284                "newpassword456",
1285            )
1286            .await
1287            .unwrap()
1288        );
1289    }
1290
1291    #[tokio::test]
1292    async fn admin_creation_stores_application_metadata_without_reserved_role_input() {
1293        let (ctx, admin, admin_session, _other, other_session) = create_admin_context().await;
1294        let expected = serde_json::json!({"preferences":{"theme":"night","nested":{"$serde_json::private::RawValue":"literal"}}});
1295        let request = make_request(
1296            HttpMethod::Post,
1297            "/admin/create-user",
1298            &admin_session.token,
1299            Some(
1300                serde_json::json!({"email":"metadata-user@fixture.test","name":"Metadata user","data":{"role":"user","preferences":expected.get("preferences").unwrap()}}),
1301            ),
1302        );
1303        let response = AdminPlugin::new()
1304            .on_request(&request, &ctx)
1305            .await
1306            .unwrap()
1307            .unwrap();
1308        assert_eq!(response.status, 200);
1309        let created = ctx
1310            .database
1311            .get_user_by_email("metadata-user@fixture.test")
1312            .await
1313            .unwrap()
1314            .unwrap();
1315        assert_eq!(created.role(), Some("user"));
1316        assert_eq!(created.metadata(), &expected);
1317        assert_eq!(
1318            json_body(&response)
1319                .get("user")
1320                .and_then(|user| user.get("id"))
1321                .and_then(serde_json::Value::as_str),
1322            Some(created.id.as_str())
1323        );
1324        assert!(
1325            ctx.database
1326                .get_session(&admin_session.token)
1327                .await
1328                .unwrap()
1329                .is_some()
1330        );
1331        assert!(
1332            ctx.database
1333                .get_session(&other_session.token)
1334                .await
1335                .unwrap()
1336                .is_some()
1337        );
1338        assert_eq!(
1339            ctx.database
1340                .get_user_by_id(&admin.id)
1341                .await
1342                .unwrap()
1343                .unwrap()
1344                .role(),
1345            Some("admin")
1346        );
1347    }
1348}
1349// LCOV_EXCL_STOP