Skip to main content

alibi_api/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::plugins::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(in crate::plugins) 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    fn static_openapi_metadata(&self) -> alibi_core::PluginOpenApiMetadata {
136        crate::metadata::plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self))
137    }
138
139    fn openapi_metadata(&self, ctx: &alibi_core::AuthInitContext<S>) -> alibi_core::PluginOpenApiMetadata {
140        crate::metadata::instance_plugin_metadata(<Self as alibi_core::AuthPlugin<S>>::name(self), &<Self as alibi_core::AuthPlugin<S>>::routes(self), ctx)
141    }
142
143        fn session_fields(&self) -> alibi_core::field_policy::FieldConfigs {
144            std::iter::once(("impersonatedBy".into(), alibi_core::field_policy::FieldConfig::new(serde_json::json!({"type":"string"})).read_only())).collect()
145        }
146
147        async fn on_init(
148            &self,
149            ctx: &mut alibi_core::AuthInitContext<S>,
150        ) -> AuthResult<()> {
151            if let Some(admin_roles) = &self.config.admin_roles {
152                let roles = self.config.roles.clone().unwrap_or_else(access::default_roles);
153                let names: Vec<_> = roles.keys().map(|name| name.to_lowercase()).collect();
154                let invalid: Vec<_> = admin_roles.iter()
155                    .filter(|role| !names.contains(&role.to_lowercase()))
156                    .map(String::as_str).collect();
157                if !invalid.is_empty() {
158                    return Err(AuthError::config(format!(
159                        "Invalid admin roles: {}. Admin roles must be defined in the 'roles' configuration.",
160                        invalid.join(", ")
161                    )));
162                }
163            }
164            if let Some(handler) = &self.config.banned_user_message_callback {
165                handler.validate::<S::User>()?;
166                ctx.extensions.insert(BannedUserMessagePolicy(handler.clone()));
167            }
168            let default_role = self.config.default_role.clone();
169            let validation_enabled = ctx.config.user_validation.is_some();
170            ctx.register_user_create_transform(move |mut input| {
171                if !validation_enabled {_ = input.banned.get_or_insert(false);}
172                _ = input.role.get_or_insert_with(|| default_role.clone());
173                Ok(input)
174            });
175            if validation_enabled {ctx.register_user_creation_adapter_default(|mut input| {
176                _ = input.banned.get_or_insert(false); Ok(input)
177            });}
178            ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
179            ctx.set_metadata(
180                "admin.default_role",
181                serde_json::Value::String(self.config.default_role.clone()),
182            );
183            ctx.set_metadata(
184                "admin.banned_user_message",
185                serde_json::Value::String(self.config.banned_user_message.clone()),
186            );
187            Ok(())
188        }
189    }
190}
191
192impl AdminPlugin {
193    /// Configure an awaited message callback over the exact stored user entity.
194    #[must_use]
195    pub fn banned_user_message_callback<U: AuthUser, H: AdminBannedUserMessage<U>>(
196        mut self,
197        handler: H,
198    ) -> Self {
199        self.config.banned_user_message_callback =
200            Some(AdminBannedUserMessageHandler::new::<U, H>(handler));
201        self
202    }
203
204    async fn require_session(
205        &self,
206        req: &AuthRequest,
207        ctx: &AuthContext<impl alibi_core::AuthSchema>,
208    ) -> AuthResult<Option<(UserView, SessionView)>> {
209        match ctx.require_authoritative_session(req).await {
210            Ok((user, session)) => Ok(Some((UserView::from(&user), SessionView::from(&session)))),
211            Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => Ok(None),
212            Err(error) => Err(error),
213        }
214    }
215
216    fn missing_session_response() -> AuthResponse {
217        AuthResponse::new(401).with_header("Content-Type", "application/json")
218    }
219
220    fn authorize(
221        &self,
222        user: &UserView,
223        resource: &str,
224        action: &str,
225        message: &str,
226    ) -> AuthResult<()> {
227        let permissions = HashMap::from([(resource.to_owned(), vec![action.to_owned()])]);
228        if has_permission(
229            Some(user.id.as_str()),
230            user.role.as_deref(),
231            &self.config,
232            &permissions,
233        ) {
234            Ok(())
235        } else {
236            Err(AuthError::forbidden(message))
237        }
238    }
239
240    async fn handle_set_role(
241        &self,
242        req: &AuthRequest,
243        ctx: &AuthContext<impl alibi_core::AuthSchema>,
244    ) -> AuthResult<AuthResponse> {
245        let body: SetRoleRequest = match validation::body(req) {
246            Ok(v) => v,
247            Err(resp) => return Ok(resp),
248        };
249        let Some((user, _session)) = self.require_session(req, ctx).await? else {
250            return Ok(Self::missing_session_response());
251        };
252        self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
253        let response = set_role_core(&body, &self.config, ctx).await?;
254        AuthResponse::json(200, &response).map_err(AuthError::from)
255    }
256
257    async fn handle_get_user(
258        &self,
259        req: &AuthRequest,
260        ctx: &AuthContext<impl alibi_core::AuthSchema>,
261    ) -> AuthResult<AuthResponse> {
262        let query = match validation::get_user(req) {
263            Ok(query) => query,
264            Err(response) => return Ok(response),
265        };
266        let Some((user, _session)) = self.require_session(req, ctx).await? else {
267            return Ok(Self::missing_session_response());
268        };
269        self.authorize(&user, "user", "get", MESSAGE_GET_USER)?;
270        let response = get_user_core(&query, ctx).await?;
271        AuthResponse::json(200, &response).map_err(AuthError::from)
272    }
273
274    async fn handle_create_user(
275        &self,
276        req: &AuthRequest,
277        ctx: &AuthContext<impl alibi_core::AuthSchema>,
278    ) -> AuthResult<AuthResponse> {
279        let body: CreateUserRequest = match validation::body(req) {
280            Ok(v) => v,
281            Err(resp) => return Ok(resp),
282        };
283        let Some((user, _session)) = self.require_session(req, ctx).await? else {
284            return Ok(Self::missing_session_response());
285        };
286        self.authorize(&user, "user", "create", MESSAGE_CREATE_USERS)?;
287        // The source checks presence after nullish body/data precedence; empty
288        // strings/arrays still request role authority. Validate nested types later.
289        if body.role.is_some()
290            || body
291                .data
292                .as_ref()
293                .is_some_and(|data| data.contains_key("role"))
294        {
295            self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
296        }
297        let response = create_user_core(&body, &self.config, ctx).await?;
298        AuthResponse::json(200, &response).map_err(AuthError::from)
299    }
300
301    async fn handle_update_user(
302        &self,
303        req: &AuthRequest,
304        ctx: &AuthContext<impl alibi_core::AuthSchema>,
305    ) -> AuthResult<AuthResponse> {
306        let mut body: AdminUpdateUserRequest = match validation::body(req) {
307            Ok(v) => v,
308            Err(resp) => return Ok(resp),
309        };
310        let Some((user, _session)) = self.require_session(req, ctx).await? else {
311            return Ok(Self::missing_session_response());
312        };
313        self.authorize(&user, "user", "update", MESSAGE_UPDATE_USERS)?;
314        let username = body
315            .data
316            .remove("username")
317            .and_then(|value| value.as_str().map(ToOwned::to_owned))
318            .map(|value| value.to_lowercase());
319        let display_username = body
320            .data
321            .remove("displayUsername")
322            .and_then(|value| value.as_str().map(ToOwned::to_owned));
323
324        if let Some(username) = username.as_deref() {
325            match validate_username(username) {
326                Ok(()) => {}
327                Err(UsernameValidationError::TooShort) => {
328                    return username_error_response(
329                        400,
330                        "USERNAME_TOO_SHORT",
331                        MESSAGE_USERNAME_TOO_SHORT,
332                    );
333                }
334                Err(UsernameValidationError::TooLong) => {
335                    return username_error_response(
336                        400,
337                        "USERNAME_IS_TOO_LONG",
338                        MESSAGE_USERNAME_TOO_LONG,
339                    );
340                }
341                Err(UsernameValidationError::Invalid) => {
342                    return username_error_response(
343                        400,
344                        "USERNAME_IS_INVALID",
345                        MESSAGE_INVALID_USERNAME,
346                    );
347                }
348            }
349
350            if let Some(existing_user) = ctx.database.get_user_by_username(username).await?
351                && AuthUser::id(&existing_user).as_ref() != body.user_id
352            {
353                return username_error_response(
354                    400,
355                    "USERNAME_IS_ALREADY_TAKEN",
356                    MESSAGE_USERNAME_IS_ALREADY_TAKEN,
357                );
358            }
359        }
360
361        if let Some(username) = username {
362            drop(
363                body.data
364                    .insert("username".to_owned(), serde_json::Value::String(username)),
365            );
366        }
367        if let Some(display_username) = display_username {
368            drop(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 = get_cookie(req, &admin_cookie_name)
562            .ok_or_else(|| AuthError::internal("Failed to find admin session"))?;
563        let admin_cookie =
564            decode_admin_session_cookie_value(ctx.config.current_secret(), &admin_cookie_value)
565                .map_err(|_error| AuthError::internal("Failed to find admin session"))?;
566
567        let (response, new_token) = stop_impersonating_core(&session, &admin_cookie, ctx).await?;
568
569        let mut auth_response = AuthResponse::json(200, &response)?;
570        auth_response = auth_response.with_appended_header(
571            "Set-Cookie",
572            create_session_cookie_with_max_age(
573                Some(&new_token),
574                if admin_cookie.dont_remember {
575                    None
576                } else {
577                    Some(ctx.config.session.expires_in.num_seconds())
578                },
579                &ctx.config,
580            )?,
581        );
582        if admin_cookie.dont_remember {
583            auth_response = auth_response.with_appended_header(
584                "Set-Cookie",
585                create_session_like_cookie(
586                    &related_cookie_name(&ctx.config, "dont_remember"),
587                    &alibi_core::utils::cookie_utils::sign_cookie_value(
588                        "true",
589                        ctx.config.current_secret(),
590                    ),
591                    None,
592                    &ctx.config,
593                )?,
594            );
595        }
596        auth_response = auth_response.with_appended_header(
597            "Set-Cookie",
598            create_clear_cookie(&admin_cookie_name, &ctx.config)?,
599        );
600        Ok(auth_response)
601    }
602
603    async fn handle_revoke_user_session(
604        &self,
605        req: &AuthRequest,
606        ctx: &AuthContext<impl alibi_core::AuthSchema>,
607    ) -> AuthResult<AuthResponse> {
608        let body: RevokeSessionRequest = match validation::body(req) {
609            Ok(v) => v,
610            Err(resp) => return Ok(resp),
611        };
612        let Some((user, _session)) = self.require_session(req, ctx).await? else {
613            return Ok(Self::missing_session_response());
614        };
615        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
616        let response = revoke_user_session_core(&body, ctx).await?;
617        AuthResponse::json(200, &response).map_err(AuthError::from)
618    }
619
620    async fn handle_revoke_user_sessions(
621        &self,
622        req: &AuthRequest,
623        ctx: &AuthContext<impl alibi_core::AuthSchema>,
624    ) -> AuthResult<AuthResponse> {
625        let body: UserIdRequest = match validation::body(req) {
626            Ok(v) => v,
627            Err(resp) => return Ok(resp),
628        };
629        let Some((user, _session)) = self.require_session(req, ctx).await? else {
630            return Ok(Self::missing_session_response());
631        };
632        self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
633        let response = revoke_user_sessions_core(&body, ctx).await?;
634        AuthResponse::json(200, &response).map_err(AuthError::from)
635    }
636
637    async fn handle_remove_user(
638        &self,
639        req: &AuthRequest,
640        ctx: &AuthContext<impl alibi_core::AuthSchema>,
641    ) -> AuthResult<AuthResponse> {
642        let body: UserIdRequest = match validation::body(req) {
643            Ok(v) => v,
644            Err(resp) => return Ok(resp),
645        };
646        let Some((user, _session)) = self.require_session(req, ctx).await? else {
647            return Ok(Self::missing_session_response());
648        };
649        self.authorize(&user, "user", "delete", MESSAGE_DELETE_USERS)?;
650        let response = remove_user_core(&body, user.id.as_str(), ctx).await?;
651        AuthResponse::json(200, &response).map_err(AuthError::from)
652    }
653
654    async fn handle_set_user_password(
655        &self,
656        req: &AuthRequest,
657        ctx: &AuthContext<impl alibi_core::AuthSchema>,
658    ) -> AuthResult<AuthResponse> {
659        let body: SetUserPasswordRequest = match validation::body(req) {
660            Ok(v) => v,
661            Err(resp) => return Ok(resp),
662        };
663        let Some((user, _session)) = self.require_session(req, ctx).await? else {
664            return Ok(Self::missing_session_response());
665        };
666        self.authorize(&user, "user", "set-password", MESSAGE_SET_USER_PASSWORD)?;
667        let response = set_user_password_core(&body, ctx).await?;
668        AuthResponse::json(200, &response).map_err(AuthError::from)
669    }
670
671    async fn handle_has_permission(
672        &self,
673        req: &AuthRequest,
674        ctx: &AuthContext<impl alibi_core::AuthSchema>,
675    ) -> AuthResult<AuthResponse> {
676        let body: HasPermissionRequest = match validation::body(req) {
677            Ok(v) => v,
678            Err(resp) => return Ok(resp),
679        };
680        if body.permissions.is_none() {
681            return AuthResponse::json(400, &serde_json::json!({"message":"invalid permission check. no permission(s) were passed."})).map_err(AuthError::from);
682        }
683        let Some((user, _session)) = self.require_session(req, ctx).await? else {
684            return Ok(Self::missing_session_response());
685        };
686        let response = has_permission_core(&body, &user, &self.config)?;
687        AuthResponse::json(200, &response).map_err(AuthError::from)
688    }
689}
690
691impl std::fmt::Debug for AdminPlugin {
692    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693        f.debug_struct("AdminPlugin").finish_non_exhaustive()
694    }
695}
696
697fn username_error_response(status: u16, code: &str, message: &str) -> AuthResult<AuthResponse> {
698    AuthResponse::json(
699        status,
700        &ErrorCodeMessageResponse {
701            code: Some(code.to_owned()),
702            message: message.to_owned(),
703        },
704    )
705    .map_err(AuthError::from)
706}
707
708pub(super) fn target_is_admin(
709    user_id: Option<&str>,
710    role: Option<&str>,
711    config: &AdminConfig,
712) -> bool {
713    is_admin_user_id(user_id, config) || is_admin_role(role, config)
714}
715
716// LCOV_EXCL_START
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use crate::plugins::test_helpers;
721    use alibi_core::entity::{AuthAccount, AuthSession};
722    use alibi_core::utils::cookie_utils::related_cookie_name;
723    use alibi_core::wire::{SessionView, UserView};
724    use alibi_core::{AuthPlugin, CreateSession, CreateUser, HttpMethod};
725    use chrono::{Duration, Utc};
726    use std::collections::HashMap;
727    use std::sync::Arc;
728
729    type TestSchema = alibi_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
730
731    async fn create_admin_context() -> (
732        AuthContext<TestSchema>,
733        UserView,
734        SessionView,
735        UserView,
736        SessionView,
737    ) {
738        let ctx = test_helpers::create_test_context().await;
739
740        let admin = test_helpers::create_user(
741            &ctx,
742            CreateUser::new()
743                .with_email("admin@example.com")
744                .with_name("Admin")
745                .with_role("admin"),
746        )
747        .await;
748        let admin_session =
749            test_helpers::create_session(&ctx, admin.id.clone(), Duration::hours(24)).await;
750
751        let user = test_helpers::create_user(
752            &ctx,
753            CreateUser::new()
754                .with_email("user@example.com")
755                .with_name("Regular User")
756                .with_role("user"),
757        )
758        .await;
759        let user_session =
760            test_helpers::create_session(&ctx, user.id.clone(), Duration::hours(24)).await;
761
762        (ctx, admin, admin_session, user, user_session)
763    }
764
765    fn make_request(
766        method: HttpMethod,
767        path: &str,
768        token: &str,
769        body: Option<serde_json::Value>,
770    ) -> AuthRequest {
771        test_helpers::create_auth_json_request_no_query(method, path, Some(token), body)
772    }
773
774    fn json_body(resp: &AuthResponse) -> serde_json::Value {
775        serde_json::from_slice(&resp.body).unwrap()
776    }
777
778    fn set_cookie_value(resp: &AuthResponse, name: &str) -> Option<String> {
779        resp.headers.get_all("Set-Cookie").find_map(|header| {
780            let (cookie_name, remainder) = header.split_once('=')?;
781            if cookie_name != name {
782                return None;
783            }
784            Some(remainder.split(';').next().unwrap_or_default().to_owned())
785        })
786    }
787
788    #[tokio::test]
789    async fn test_custom_admin_role_can_use_permission_engine() {
790        let config = Arc::new(alibi_core::AuthConfig::new(
791            "test-secret-key-at-least-32-chars-long",
792        ));
793        let database = test_helpers::create_test_database().await;
794        let ctx = AuthContext::new(config, Arc::clone(&database));
795
796        let admin = database
797            .create_user(
798                CreateUser::new()
799                    .with_email("superadmin@example.com")
800                    .with_name("Super Admin")
801                    .with_role("superadmin"),
802            )
803            .await
804            .unwrap();
805
806        let admin_session = database
807            .create_session(CreateSession {
808                additional_fields: alibi_core::field_policy::FieldValues::default(),
809                token: None,
810                active_team_id: None,
811                user_id: admin.id.clone(),
812                expires_at: Utc::now() + Duration::hours(24),
813                ip_address: None,
814                user_agent: None,
815                impersonated_by: None,
816                active_organization_id: None,
817            })
818            .await
819            .unwrap();
820
821        let _user = database
822            .create_user(
823                CreateUser::new()
824                    .with_email("user@example.com")
825                    .with_name("User")
826                    .with_role("user"),
827            )
828            .await
829            .unwrap();
830
831        let plugin = AdminPlugin::with_config(AdminConfig {
832            admin_roles: Some(vec!["superadmin".to_owned()]),
833            roles: Some(HashMap::from([(
834                "superadmin".to_owned(),
835                RolePermissions::new()
836                    .allow(
837                        "user",
838                        [
839                            "create",
840                            "list",
841                            "set-role",
842                            "ban",
843                            "impersonate",
844                            "delete",
845                            "set-password",
846                            "get",
847                            "update",
848                        ],
849                    )
850                    .allow("session", ["list", "revoke", "delete"]),
851            )])),
852            ..Default::default()
853        });
854
855        let req = make_request(
856            HttpMethod::Get,
857            "/admin/list-users",
858            &admin_session.token,
859            None,
860        );
861
862        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
863        assert_eq!(resp.status, 200);
864        assert_eq!(
865            (*(json_body(&resp))
866                .get("total")
867                .unwrap_or(&serde_json::Value::Null)),
868            2
869        );
870    }
871
872    #[tokio::test]
873    async fn test_ban_revokes_user_sessions() {
874        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
875        let plugin = AdminPlugin::new();
876
877        let sessions = ctx.database.get_user_sessions(&user.id).await.unwrap();
878        assert_ne!(sessions.len(), 0);
879
880        let req = make_request(
881            HttpMethod::Post,
882            "/admin/ban-user",
883            &admin_session.token,
884            Some(serde_json::json!({
885                "userId": user.id,
886                "banReason": "bad behavior"
887            })),
888        );
889
890        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
891        assert_eq!(resp.status, 200);
892
893        let sessions_2 = ctx.database.get_user_sessions(&user.id).await.unwrap();
894        assert_eq!(sessions_2.len(), 0);
895    }
896
897    #[tokio::test]
898    async fn test_unban_clears_ban_reason_and_expires() {
899        let (ctx, _admin, admin_session, user, _user_session) = create_admin_context().await;
900        let plugin = AdminPlugin::new();
901
902        let req = make_request(
903            HttpMethod::Post,
904            "/admin/ban-user",
905            &admin_session.token,
906            Some(serde_json::json!({
907                "userId": user.id,
908                "banReason": "spam",
909                "banExpiresIn": 3600
910            })),
911        );
912        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
913        assert_eq!(resp.status, 200);
914
915        let req_2 = make_request(
916            HttpMethod::Post,
917            "/admin/unban-user",
918            &admin_session.token,
919            Some(serde_json::json!({
920                "userId": user.id,
921            })),
922        );
923
924        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
925        assert_eq!(resp_2.status, 200);
926
927        let updated_user = ctx
928            .database
929            .get_user_by_id(&user.id)
930            .await
931            .unwrap()
932            .unwrap();
933        assert_eq!(updated_user.banned, Some(false));
934        assert!(updated_user.ban_reason.is_none());
935        assert!(updated_user.ban_expires.is_none());
936    }
937
938    #[tokio::test]
939    async fn test_impersonation_session_tracks_admin_id() {
940        let (ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
941        let plugin = AdminPlugin::new();
942
943        let req = make_request(
944            HttpMethod::Post,
945            "/admin/impersonate-user",
946            &admin_session.token,
947            Some(serde_json::json!({
948                "userId": user.id,
949            })),
950        );
951
952        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
953        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
954        assert!(
955            set_cookie_value(&resp, &admin_cookie_name).is_some(),
956            "impersonation should emit an admin_session cookie"
957        );
958        let token = (*(*(json_body(&resp))
959            .get("session")
960            .unwrap_or(&serde_json::Value::Null))
961        .get("token")
962        .unwrap_or(&serde_json::Value::Null))
963        .as_str()
964        .unwrap()
965        .to_owned();
966        let session = ctx.database.get_session(&token).await.unwrap().unwrap();
967
968        assert_eq!(session.impersonated_by().unwrap(), admin.id);
969    }
970
971    #[tokio::test]
972    async fn test_stop_impersonating_restores_admin_session() {
973        let (mut ctx, admin, admin_session, user, _user_session) = create_admin_context().await;
974        let plugin = AdminPlugin::new();
975        let mut init =
976            alibi_core::AuthInitContext::new(Arc::clone(&ctx.config), Arc::clone(&ctx.database));
977        plugin.on_init(&mut init).await.unwrap();
978        ctx.metadata.extend(init.into_parts().metadata);
979
980        let req = make_request(
981            HttpMethod::Post,
982            "/admin/impersonate-user",
983            &admin_session.token,
984            Some(serde_json::json!({
985                "userId": user.id,
986            })),
987        );
988        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
989        let impersonation_token = (*(*(json_body(&resp))
990            .get("session")
991            .unwrap_or(&serde_json::Value::Null))
992        .get("token")
993        .unwrap_or(&serde_json::Value::Null))
994        .as_str()
995        .unwrap()
996        .to_owned();
997        let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
998        let admin_cookie = set_cookie_value(&resp, &admin_cookie_name)
999            .expect("impersonation should set the admin_session cookie");
1000
1001        let mut req_2 = make_request(
1002            HttpMethod::Post,
1003            "/admin/stop-impersonating",
1004            &impersonation_token,
1005            None,
1006        );
1007        req_2.headers.insert(
1008            "cookie".to_owned(),
1009            format!(
1010                "{}; {admin_cookie_name}={admin_cookie}",
1011                req_2
1012                    .headers
1013                    .get("cookie")
1014                    .expect("impersonated browser has a signed session cookie")
1015            ),
1016        );
1017        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1018        let body = json_body(&resp_2);
1019
1020        let restored_token = (*(*(body).get("session").unwrap_or(&serde_json::Value::Null))
1021            .get("token")
1022            .unwrap_or(&serde_json::Value::Null))
1023        .as_str()
1024        .unwrap();
1025        assert_eq!(
1026            restored_token, admin_session.token,
1027            "stop-impersonating should restore the original admin session token"
1028        );
1029        let restored_session = ctx
1030            .database
1031            .get_session(restored_token)
1032            .await
1033            .unwrap()
1034            .unwrap();
1035        assert_eq!(restored_session.user_id, admin.id);
1036        assert!(restored_session.impersonated_by.is_none());
1037        assert!(
1038            ctx.database
1039                .get_session(&impersonation_token)
1040                .await
1041                .unwrap()
1042                .is_none()
1043        );
1044        let cleared_admin_cookie = resp_2
1045            .headers
1046            .get_all("Set-Cookie")
1047            .find(|header| header.starts_with(&format!("{admin_cookie_name}=")))
1048            .expect("stop-impersonating should clear admin_session");
1049        assert!(
1050            cleared_admin_cookie.contains("Max-Age=0"),
1051            "admin_session should be cleared after stop-impersonating"
1052        );
1053    }
1054
1055    #[tokio::test]
1056    async fn test_list_user_sessions_missing_user_returns_empty_array() {
1057        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1058        let plugin = AdminPlugin::new();
1059
1060        let req = make_request(
1061            HttpMethod::Post,
1062            "/admin/list-user-sessions",
1063            &admin_session.token,
1064            Some(serde_json::json!({
1065                "userId": "missing-user",
1066            })),
1067        );
1068        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1069
1070        assert_eq!(resp.status, 200);
1071        assert_eq!(json_body(&resp), serde_json::json!({ "sessions": [] }));
1072    }
1073
1074    #[tokio::test]
1075    async fn test_revoke_user_sessions_missing_user_still_succeeds() {
1076        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1077        let plugin = AdminPlugin::new();
1078
1079        let req = make_request(
1080            HttpMethod::Post,
1081            "/admin/revoke-user-sessions",
1082            &admin_session.token,
1083            Some(serde_json::json!({
1084                "userId": "missing-user",
1085            })),
1086        );
1087        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1088
1089        assert_eq!(resp.status, 200);
1090        assert_eq!(json_body(&resp), serde_json::json!({ "success": true }));
1091    }
1092
1093    #[tokio::test]
1094    async fn test_stop_impersonating_without_impersonated_session_returns_bad_request() {
1095        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1096        let plugin = AdminPlugin::new();
1097
1098        let req = make_request(
1099            HttpMethod::Post,
1100            "/admin/stop-impersonating",
1101            &admin_session.token,
1102            None,
1103        );
1104        let err = plugin.on_request(&req, &ctx).await.unwrap_err();
1105        let response = err.to_auth_response();
1106        assert_eq!(response.status, 400);
1107        assert_eq!(
1108            json_body(&response),
1109            serde_json::json!({
1110                // Upstream throws this via `APIError.fromStatus`, which carries no
1111                // code — only errors built from a code constant have one.
1112                "message": "You are not impersonating anyone"
1113            })
1114        );
1115    }
1116
1117    #[tokio::test]
1118    async fn test_remove_user_cleans_up_sessions_and_accounts() {
1119        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1120        let plugin = AdminPlugin::new();
1121
1122        let req = make_request(
1123            HttpMethod::Post,
1124            "/admin/create-user",
1125            &admin_session.token,
1126            Some(serde_json::json!({
1127                "email": "tobedeleted@example.com",
1128                "password": "securepassword123",
1129                "name": "To Be Deleted"
1130            })),
1131        );
1132        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1133        let user_id = (*(*(json_body(&resp))
1134            .get("user")
1135            .unwrap_or(&serde_json::Value::Null))
1136        .get("id")
1137        .unwrap_or(&serde_json::Value::Null))
1138        .as_str()
1139        .unwrap()
1140        .to_owned();
1141
1142        let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
1143        assert_eq!(accounts.len(), 1);
1144
1145        let req_2 = make_request(
1146            HttpMethod::Post,
1147            "/admin/remove-user",
1148            &admin_session.token,
1149            Some(serde_json::json!({
1150                "userId": user_id,
1151            })),
1152        );
1153        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1154        assert_eq!(resp_2.status, 200);
1155
1156        assert!(
1157            ctx.database
1158                .get_user_by_id(&user_id)
1159                .await
1160                .unwrap()
1161                .is_none()
1162        );
1163        assert_eq!(
1164            ctx.database
1165                .get_user_accounts(&user_id)
1166                .await
1167                .unwrap()
1168                .len(),
1169            0
1170        );
1171    }
1172
1173    #[tokio::test]
1174    async fn test_set_user_password_updates_credential_account() {
1175        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1176        let plugin = AdminPlugin::new();
1177
1178        let req = make_request(
1179            HttpMethod::Post,
1180            "/admin/create-user",
1181            &admin_session.token,
1182            Some(serde_json::json!({
1183                "email": "pwuser@example.com",
1184                "password": "oldpassword123",
1185                "name": "PW User"
1186            })),
1187        );
1188        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1189        let user_id = (*(*(json_body(&resp))
1190            .get("user")
1191            .unwrap_or(&serde_json::Value::Null))
1192        .get("id")
1193        .unwrap_or(&serde_json::Value::Null))
1194        .as_str()
1195        .unwrap()
1196        .to_owned();
1197
1198        let before = ctx.database.get_user_accounts(&user_id).await.unwrap();
1199        let old_password = (*(before)
1200            .first()
1201            .expect("persisted rows contain the requested index"))
1202        .password()
1203        .unwrap()
1204        .to_owned();
1205
1206        let req_2 = make_request(
1207            HttpMethod::Post,
1208            "/admin/set-user-password",
1209            &admin_session.token,
1210            Some(serde_json::json!({
1211                "userId": user_id,
1212                "newPassword": "newpassword456"
1213            })),
1214        );
1215        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1216        assert_eq!(resp_2.status, 200);
1217
1218        let after = ctx.database.get_user_accounts(&user_id).await.unwrap();
1219        let new_password = (*(after)
1220            .first()
1221            .expect("persisted rows contain the requested index"))
1222        .password()
1223        .unwrap()
1224        .to_owned();
1225        assert_ne!(old_password, new_password);
1226    }
1227
1228    #[tokio::test]
1229    async fn test_set_user_password_creates_canonical_credential_when_missing() {
1230        let (ctx, _admin, admin_session, _user, _user_session) = create_admin_context().await;
1231        let plugin = AdminPlugin::new();
1232
1233        let req = make_request(
1234            HttpMethod::Post,
1235            "/admin/create-user",
1236            &admin_session.token,
1237            Some(serde_json::json!({
1238                "email": "passwordless@example.com",
1239                "name": "Passwordless User"
1240            })),
1241        );
1242        let resp = plugin.on_request(&req, &ctx).await.unwrap().unwrap();
1243        let user_id = (*(*(json_body(&resp))
1244            .get("user")
1245            .unwrap_or(&serde_json::Value::Null))
1246        .get("id")
1247        .unwrap_or(&serde_json::Value::Null))
1248        .as_str()
1249        .unwrap()
1250        .to_owned();
1251
1252        let req_2 = make_request(
1253            HttpMethod::Post,
1254            "/admin/set-user-password",
1255            &admin_session.token,
1256            Some(serde_json::json!({
1257                "userId": user_id,
1258                "newPassword": "newpassword456"
1259            })),
1260        );
1261        let resp_2 = plugin.on_request(&req_2, &ctx).await.unwrap().unwrap();
1262        assert_eq!(resp_2.status, 200);
1263
1264        let accounts = ctx.database.get_user_accounts(&user_id).await.unwrap();
1265        assert_eq!(accounts.len(), 1);
1266        let account = accounts
1267            .first()
1268            .expect("password setting creates a credential");
1269        assert_eq!(account.user_id(), user_id);
1270        assert_eq!(account.account_id(), user_id);
1271        assert_eq!(account.provider_id(), "credential");
1272        assert!(
1273            alibi_core::PasswordHasher::verify(
1274                &alibi_core::ScryptHasher,
1275                account.password().unwrap(),
1276                "newpassword456",
1277            )
1278            .await
1279            .unwrap()
1280        );
1281    }
1282
1283    #[tokio::test]
1284    async fn admin_creation_stores_application_metadata_without_reserved_role_input() {
1285        let (ctx, admin, admin_session, _other, other_session) = create_admin_context().await;
1286        let expected = serde_json::json!({"preferences":{"theme":"night","nested":{"$serde_json::private::RawValue":"literal"}}});
1287        let request = make_request(
1288            HttpMethod::Post,
1289            "/admin/create-user",
1290            &admin_session.token,
1291            Some(
1292                serde_json::json!({"email":"metadata-user@fixture.test","name":"Metadata user","data":{"role":"user","preferences":expected.get("preferences").unwrap()}}),
1293            ),
1294        );
1295        let response = AdminPlugin::new()
1296            .on_request(&request, &ctx)
1297            .await
1298            .unwrap()
1299            .unwrap();
1300        assert_eq!(response.status, 200);
1301        let created = ctx
1302            .database
1303            .get_user_by_email("metadata-user@fixture.test")
1304            .await
1305            .unwrap()
1306            .unwrap();
1307        assert_eq!(created.role(), Some("user"));
1308        assert_eq!(created.metadata(), &expected);
1309        assert_eq!(
1310            json_body(&response)
1311                .get("user")
1312                .and_then(|user| user.get("id"))
1313                .and_then(serde_json::Value::as_str),
1314            Some(created.id.as_str())
1315        );
1316        assert!(
1317            ctx.database
1318                .get_session(&admin_session.token)
1319                .await
1320                .unwrap()
1321                .is_some()
1322        );
1323        assert!(
1324            ctx.database
1325                .get_session(&other_session.token)
1326                .await
1327                .unwrap()
1328                .is_some()
1329        );
1330        assert_eq!(
1331            ctx.database
1332                .get_user_by_id(&admin.id)
1333                .await
1334                .unwrap()
1335                .unwrap()
1336                .role(),
1337            Some("admin")
1338        );
1339    }
1340}
1341// LCOV_EXCL_STOP