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