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