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};
38pub type AdminImpersonationResponse =
40 types::SessionUserResponse<alibi_core::wire::SessionView, alibi_core::wire::UserView>;
41pub type AdminUserResponse = types::UserResponse<AdminUserOutput>;
43pub type AdminListUsersResponse = types::ListUsersResponse<AdminUserOutput>;
45pub 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
74pub struct AdminPlugin {
76 config: AdminConfig,
77}
78
79#[derive(Debug, Clone, alibi_core::PluginConfig)]
81#[plugin(name = "AdminPlugin")]
82pub struct AdminConfig {
83 #[config(default = "user".to_owned())]
85 pub default_role: String,
86 #[config(default = None)]
89 pub admin_roles: Option<Vec<String>>,
90 #[config(default = None)]
92 pub admin_user_ids: Option<Vec<String>>,
93 #[config(default = None)]
97 pub roles: Option<HashMap<String, RolePermissions>>,
98 #[config(default = None)]
100 pub default_ban_reason: Option<String>,
101 #[config(default = None)]
103 pub default_ban_expires_in: Option<f64>,
104 #[config(default = None)]
106 pub impersonation_session_duration: Option<f64>,
107 #[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 #[config(skip, default = None)]
113 pub banned_user_message_callback: Option<AdminBannedUserMessageHandler>,
114 #[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 #[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 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#[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 "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