Skip to main content

alibi_plugins/admin/
mod.rs

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