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