1use std::collections::HashMap;
2
3use better_auth_core::entity::AuthUser;
4use better_auth_core::utils::cookie_utils::{
5 create_clear_cookie, create_session_cookie_with_max_age, create_session_like_cookie,
6 related_cookie_name,
7};
8use better_auth_core::utils::username::{UsernameValidationError, validate_username};
9use better_auth_core::wire::{SessionView, UserView};
10use better_auth_core::{
11 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, ErrorCodeMessageResponse,
12};
13use validator::Validate;
14
15pub mod access;
16pub(super) mod handlers;
17pub(super) mod types;
18
19#[cfg(test)]
20mod tests;
21
22use crate::plugins::helpers::{delete_session_cookie_headers, get_cookie};
23use access::{has_permission, is_admin_role, is_admin_user_id};
24use handlers::*;
25use types::*;
26
27const MESSAGE_CHANGE_ROLE: &str = "You are not allowed to change users role";
28const MESSAGE_CREATE_USERS: &str = "You are not allowed to create users";
29const MESSAGE_LIST_USERS: &str = "You are not allowed to list users";
30const MESSAGE_LIST_USER_SESSIONS: &str = "You are not allowed to list users sessions";
31const MESSAGE_BAN_USERS: &str = "You are not allowed to ban users";
32const MESSAGE_IMPERSONATE_USERS: &str = "You are not allowed to impersonate users";
33const MESSAGE_REVOKE_USER_SESSIONS: &str = "You are not allowed to revoke users sessions";
34const MESSAGE_DELETE_USERS: &str = "You are not allowed to delete users";
35const MESSAGE_SET_USER_PASSWORD: &str = "You are not allowed to set users password";
36const MESSAGE_GET_USER: &str = "You are not allowed to get user";
37const MESSAGE_UPDATE_USERS: &str = "You are not allowed to update users";
38const MESSAGE_USERNAME_IS_ALREADY_TAKEN: &str = "Username is already taken. Please try another.";
39const MESSAGE_USERNAME_TOO_SHORT: &str = "Username is too short";
40const MESSAGE_USERNAME_TOO_LONG: &str = "Username is too long";
41const MESSAGE_INVALID_USERNAME: &str = "Username is invalid";
42
43fn username_error_response(status: u16, code: &str, message: &str) -> AuthResult<AuthResponse> {
44 AuthResponse::json(
45 status,
46 &ErrorCodeMessageResponse {
47 code: code.to_string(),
48 message: message.to_string(),
49 },
50 )
51 .map_err(AuthError::from)
52}
53
54pub struct AdminPlugin {
56 config: AdminConfig,
57}
58
59#[derive(Debug, Clone, better_auth_core::PluginConfig)]
61#[plugin(name = "AdminPlugin")]
62pub struct AdminConfig {
63 #[config(default = "user".to_string())]
65 pub default_role: String,
66 #[config(default = vec!["admin".to_string()])]
68 pub admin_roles: Vec<String>,
69 #[config(default = None)]
71 pub admin_user_ids: Option<Vec<String>>,
72 #[config(default = HashMap::new())]
75 pub roles: HashMap<String, access::RolePermissions>,
76 #[config(default = None)]
78 pub default_ban_reason: Option<String>,
79 #[config(default = None)]
81 pub default_ban_expires_in: Option<i64>,
82 #[config(default = None)]
84 pub impersonation_session_duration: Option<i64>,
85 #[config(default = "You have been banned from this application. Please contact support if you believe this is an error.".to_string())]
87 pub banned_user_message: String,
88 #[config(default = false)]
90 pub allow_impersonating_admins: bool,
91}
92
93better_auth_core::impl_auth_plugin! {
94 AdminPlugin, "admin";
95 routes {
96 post "/admin/set-role" => handle_set_role, "admin_set_role";
97 get "/admin/get-user" => handle_get_user, "admin_get_user";
98 post "/admin/create-user" => handle_create_user, "admin_create_user";
99 post "/admin/update-user" => handle_update_user, "admin_update_user";
100 get "/admin/list-users" => handle_list_users, "admin_list_users";
101 post "/admin/list-user-sessions" => handle_list_user_sessions, "admin_list_user_sessions";
102 post "/admin/ban-user" => handle_ban_user, "admin_ban_user";
103 post "/admin/unban-user" => handle_unban_user, "admin_unban_user";
104 post "/admin/impersonate-user" => handle_impersonate_user, "admin_impersonate_user";
105 post "/admin/stop-impersonating" => handle_stop_impersonating, "admin_stop_impersonating";
106 post "/admin/revoke-user-session" => handle_revoke_user_session, "admin_revoke_user_session";
107 post "/admin/revoke-user-sessions" => handle_revoke_user_sessions, "admin_revoke_user_sessions";
108 post "/admin/remove-user" => handle_remove_user, "admin_remove_user";
109 post "/admin/set-user-password" => handle_set_user_password, "admin_set_user_password";
110 post "/admin/has-permission" => handle_has_permission, "admin_has_permission";
111 }
112 extra {
113 async fn on_init(
114 &self,
115 ctx: &mut better_auth_core::AuthInitContext<S>,
116 ) -> better_auth_core::AuthResult<()> {
117 ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
118 ctx.set_metadata(
119 "admin.default_role",
120 serde_json::Value::String(self.config.default_role.clone()),
121 );
122 ctx.set_metadata(
123 "admin.banned_user_message",
124 serde_json::Value::String(self.config.banned_user_message.clone()),
125 );
126 Ok(())
127 }
128 }
129}
130
131impl AdminPlugin {
132 async fn require_session(
133 &self,
134 req: &AuthRequest,
135 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
136 ) -> AuthResult<(UserView, SessionView)> {
137 let (user, session) = ctx.require_session(req).await?;
138 Ok((UserView::from(&user), SessionView::from(&session)))
139 }
140
141 fn authorize(
142 &self,
143 user: &UserView,
144 resource: &str,
145 action: &str,
146 message: &str,
147 ) -> AuthResult<()> {
148 let permissions = HashMap::from([(resource.to_string(), vec![action.to_string()])]);
149 if has_permission(
150 Some(user.id.as_str()),
151 user.role.as_deref(),
152 &self.config,
153 &permissions,
154 ) {
155 Ok(())
156 } else {
157 Err(AuthError::forbidden(message))
158 }
159 }
160
161 async fn handle_set_role(
162 &self,
163 req: &AuthRequest,
164 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
165 ) -> AuthResult<AuthResponse> {
166 let (user, _session) = self.require_session(req, ctx).await?;
167 self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
168 let body: SetRoleRequest = match better_auth_core::validate_request_body(req) {
169 Ok(v) => v,
170 Err(resp) => return Ok(resp),
171 };
172 let response = set_role_core(&body, &self.config, ctx).await?;
173 AuthResponse::json(200, &response).map_err(AuthError::from)
174 }
175
176 async fn handle_get_user(
177 &self,
178 req: &AuthRequest,
179 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
180 ) -> AuthResult<AuthResponse> {
181 let (user, _session) = self.require_session(req, ctx).await?;
182 self.authorize(&user, "user", "get", MESSAGE_GET_USER)?;
183 let query = GetUserQuery {
184 id: req.query.get("id").cloned().unwrap_or_default(),
185 };
186 query
187 .validate()
188 .map_err(|error| AuthError::validation(error.to_string()))?;
189 let response = get_user_core(&query, ctx).await?;
190 AuthResponse::json(200, &response).map_err(AuthError::from)
191 }
192
193 async fn handle_create_user(
194 &self,
195 req: &AuthRequest,
196 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
197 ) -> AuthResult<AuthResponse> {
198 let (user, _session) = self.require_session(req, ctx).await?;
199 self.authorize(&user, "user", "create", MESSAGE_CREATE_USERS)?;
200 let body: CreateUserRequest = match better_auth_core::validate_request_body(req) {
201 Ok(v) => v,
202 Err(resp) => return Ok(resp),
203 };
204 let response = create_user_core(&body, &self.config, ctx).await?;
205 AuthResponse::json(200, &response).map_err(AuthError::from)
206 }
207
208 async fn handle_update_user(
209 &self,
210 req: &AuthRequest,
211 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
212 ) -> AuthResult<AuthResponse> {
213 let (user, _session) = self.require_session(req, ctx).await?;
214 self.authorize(&user, "user", "update", MESSAGE_UPDATE_USERS)?;
215 let mut body: AdminUpdateUserRequest = match better_auth_core::validate_request_body(req) {
216 Ok(v) => v,
217 Err(resp) => return Ok(resp),
218 };
219 let username = body
220 .data
221 .remove("username")
222 .and_then(|value| value.as_str().map(ToOwned::to_owned))
223 .map(|value| value.to_lowercase());
224 let display_username = body
225 .data
226 .remove("displayUsername")
227 .and_then(|value| value.as_str().map(ToOwned::to_owned));
228
229 if let Some(username) = username.as_deref() {
230 match validate_username(username) {
231 Ok(()) => {}
232 Err(UsernameValidationError::TooShort) => {
233 return username_error_response(
234 400,
235 "USERNAME_TOO_SHORT",
236 MESSAGE_USERNAME_TOO_SHORT,
237 );
238 }
239 Err(UsernameValidationError::TooLong) => {
240 return username_error_response(
241 400,
242 "USERNAME_IS_TOO_LONG",
243 MESSAGE_USERNAME_TOO_LONG,
244 );
245 }
246 Err(UsernameValidationError::Invalid) => {
247 return username_error_response(
248 400,
249 "USERNAME_IS_INVALID",
250 MESSAGE_INVALID_USERNAME,
251 );
252 }
253 }
254
255 if let Some(existing_user) = ctx.database.get_user_by_username(username).await?
256 && AuthUser::id(&existing_user).as_ref() != body.user_id
257 {
258 return username_error_response(
259 400,
260 "USERNAME_IS_ALREADY_TAKEN",
261 MESSAGE_USERNAME_IS_ALREADY_TAKEN,
262 );
263 }
264 }
265
266 if let Some(username) = username {
267 _ = body
268 .data
269 .insert("username".to_string(), serde_json::Value::String(username));
270 }
271 if let Some(display_username) = display_username {
272 _ = body.data.insert(
273 "displayUsername".to_string(),
274 serde_json::Value::String(display_username),
275 );
276 }
277
278 let response = update_user_core(&body, &user, &self.config, ctx).await?;
279 AuthResponse::json(200, &response).map_err(AuthError::from)
280 }
281
282 async fn handle_list_users(
283 &self,
284 req: &AuthRequest,
285 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
286 ) -> AuthResult<AuthResponse> {
287 let (user, _session) = self.require_session(req, ctx).await?;
288 self.authorize(&user, "user", "list", MESSAGE_LIST_USERS)?;
289 let query = ListUsersQueryParams {
290 limit: req.query.get("limit").and_then(|value| value.parse().ok()),
291 offset: req.query.get("offset").and_then(|value| value.parse().ok()),
292 search_field: req.query.get("searchField").cloned(),
293 search_value: req.query.get("searchValue").cloned(),
294 search_operator: req.query.get("searchOperator").cloned(),
295 sort_by: req.query.get("sortBy").cloned(),
296 sort_direction: req.query.get("sortDirection").cloned(),
297 filter_field: req.query.get("filterField").cloned(),
298 filter_value: req.query.get("filterValue").cloned(),
299 filter_operator: req.query.get("filterOperator").cloned(),
300 };
301 let response = list_users_core(&query, ctx).await?;
302 AuthResponse::json(200, &response).map_err(AuthError::from)
303 }
304
305 async fn handle_list_user_sessions(
306 &self,
307 req: &AuthRequest,
308 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
309 ) -> AuthResult<AuthResponse> {
310 let (user, _session) = self.require_session(req, ctx).await?;
311 self.authorize(&user, "session", "list", MESSAGE_LIST_USER_SESSIONS)?;
312 let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
313 Ok(v) => v,
314 Err(resp) => return Ok(resp),
315 };
316 let response = list_user_sessions_core(&body, ctx).await?;
317 AuthResponse::json(200, &response).map_err(AuthError::from)
318 }
319
320 async fn handle_ban_user(
321 &self,
322 req: &AuthRequest,
323 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
324 ) -> AuthResult<AuthResponse> {
325 let (user, _session) = self.require_session(req, ctx).await?;
326 self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
327 let body: BanUserRequest = match better_auth_core::validate_request_body(req) {
328 Ok(v) => v,
329 Err(resp) => return Ok(resp),
330 };
331 let response = ban_user_core(&body, user.id.as_str(), &self.config, ctx).await?;
332 AuthResponse::json(200, &response).map_err(AuthError::from)
333 }
334
335 async fn handle_unban_user(
336 &self,
337 req: &AuthRequest,
338 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
339 ) -> AuthResult<AuthResponse> {
340 let (user, _session) = self.require_session(req, ctx).await?;
341 self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
342 let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
343 Ok(v) => v,
344 Err(resp) => return Ok(resp),
345 };
346 let response = unban_user_core(&body, ctx).await?;
347 AuthResponse::json(200, &response).map_err(AuthError::from)
348 }
349
350 async fn handle_impersonate_user(
351 &self,
352 req: &AuthRequest,
353 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
354 ) -> AuthResult<AuthResponse> {
355 let (user, session) = self.require_session(req, ctx).await?;
356 self.authorize(&user, "user", "impersonate", MESSAGE_IMPERSONATE_USERS)?;
357 let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
358 Ok(v) => v,
359 Err(resp) => return Ok(resp),
360 };
361 let (response, token) = impersonate_user_core(
362 &body,
363 user.id.as_str(),
364 req.headers
365 .get("x-forwarded-for")
366 .map(|value| value.as_str()),
367 req.headers.get("user-agent").map(|value| value.as_str()),
368 &self.config,
369 ctx,
370 )
371 .await?;
372 let dont_remember =
373 get_cookie(req, &related_cookie_name(&ctx.config, "dont_remember")).is_some();
374 let admin_cookie = create_admin_session_cookie_value(
375 &ctx.config.secret,
376 &AdminSessionCookiePayload {
377 session_token: session.token.clone(),
378 dont_remember,
379 },
380 ctx.config.session.expires_in,
381 )?;
382 let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
383
384 let mut auth_response = AuthResponse::json(200, &response)?;
385 for cookie in delete_session_cookie_headers(&ctx.config) {
386 auth_response = auth_response.with_appended_header("Set-Cookie", cookie);
387 }
388 auth_response = auth_response.with_appended_header(
389 "Set-Cookie",
390 create_session_like_cookie(
391 &admin_cookie_name,
392 &admin_cookie,
393 Some(ctx.config.session.expires_in.num_seconds()),
394 &ctx.config,
395 ),
396 );
397 auth_response = auth_response.with_appended_header(
398 "Set-Cookie",
399 create_session_cookie_with_max_age(Some(&token), None, &ctx.config),
400 );
401 auth_response = auth_response.with_appended_header(
402 "Set-Cookie",
403 create_session_like_cookie(
404 &related_cookie_name(&ctx.config, "dont_remember"),
405 "true",
406 None,
407 &ctx.config,
408 ),
409 );
410 Ok(auth_response)
411 }
412
413 async fn handle_stop_impersonating(
414 &self,
415 req: &AuthRequest,
416 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
417 ) -> AuthResult<AuthResponse> {
418 let session_manager = ctx.session_manager();
419 let token = session_manager
420 .extract_session_token(req)
421 .ok_or(AuthError::Unauthenticated)?;
422 let session = session_manager
423 .get_session(&token)
424 .await?
425 .ok_or(AuthError::Unauthenticated)?;
426 let session = SessionView::from(&session);
427 if session.impersonated_by.is_none() {
428 return Err(AuthError::bad_request("You are not impersonating anyone"));
429 }
430
431 let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
432 let admin_cookie_value = get_cookie(req, &admin_cookie_name)
433 .ok_or_else(|| AuthError::internal("Failed to find admin session"))?;
434 let admin_cookie =
435 decode_admin_session_cookie_value(&ctx.config.secret, &admin_cookie_value)
436 .map_err(|_| AuthError::internal("Failed to find admin session"))?;
437
438 let (response, new_token) = stop_impersonating_core(&session, &admin_cookie, ctx).await?;
439
440 let mut auth_response = AuthResponse::json(200, &response)?;
441 auth_response = auth_response.with_appended_header(
442 "Set-Cookie",
443 create_session_cookie_with_max_age(
444 Some(&new_token),
445 if admin_cookie.dont_remember {
446 None
447 } else {
448 Some(ctx.config.session.expires_in.num_seconds())
449 },
450 &ctx.config,
451 ),
452 );
453 if admin_cookie.dont_remember {
454 auth_response = auth_response.with_appended_header(
455 "Set-Cookie",
456 create_session_like_cookie(
457 &related_cookie_name(&ctx.config, "dont_remember"),
458 "true",
459 None,
460 &ctx.config,
461 ),
462 );
463 }
464 auth_response = auth_response.with_appended_header(
465 "Set-Cookie",
466 create_clear_cookie(&admin_cookie_name, &ctx.config),
467 );
468 Ok(auth_response)
469 }
470
471 async fn handle_revoke_user_session(
472 &self,
473 req: &AuthRequest,
474 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
475 ) -> AuthResult<AuthResponse> {
476 let (user, _session) = self.require_session(req, ctx).await?;
477 self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
478 let body: RevokeSessionRequest = match better_auth_core::validate_request_body(req) {
479 Ok(v) => v,
480 Err(resp) => return Ok(resp),
481 };
482 let response = revoke_user_session_core(&body, ctx).await?;
483 AuthResponse::json(200, &response).map_err(AuthError::from)
484 }
485
486 async fn handle_revoke_user_sessions(
487 &self,
488 req: &AuthRequest,
489 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
490 ) -> AuthResult<AuthResponse> {
491 let (user, _session) = self.require_session(req, ctx).await?;
492 self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
493 let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
494 Ok(v) => v,
495 Err(resp) => return Ok(resp),
496 };
497 let response = revoke_user_sessions_core(&body, ctx).await?;
498 AuthResponse::json(200, &response).map_err(AuthError::from)
499 }
500
501 async fn handle_remove_user(
502 &self,
503 req: &AuthRequest,
504 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
505 ) -> AuthResult<AuthResponse> {
506 let (user, _session) = self.require_session(req, ctx).await?;
507 self.authorize(&user, "user", "delete", MESSAGE_DELETE_USERS)?;
508 let body: UserIdRequest = match better_auth_core::validate_request_body(req) {
509 Ok(v) => v,
510 Err(resp) => return Ok(resp),
511 };
512 let response = remove_user_core(&body, user.id.as_str(), ctx).await?;
513 AuthResponse::json(200, &response).map_err(AuthError::from)
514 }
515
516 async fn handle_set_user_password(
517 &self,
518 req: &AuthRequest,
519 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
520 ) -> AuthResult<AuthResponse> {
521 let (user, _session) = self.require_session(req, ctx).await?;
522 self.authorize(&user, "user", "set-password", MESSAGE_SET_USER_PASSWORD)?;
523 let body: SetUserPasswordRequest = match better_auth_core::validate_request_body(req) {
524 Ok(v) => v,
525 Err(resp) => return Ok(resp),
526 };
527 let response = set_user_password_core(&body, ctx).await?;
528 AuthResponse::json(200, &response).map_err(AuthError::from)
529 }
530
531 async fn handle_has_permission(
532 &self,
533 req: &AuthRequest,
534 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
535 ) -> AuthResult<AuthResponse> {
536 let (user, _session) = self.require_session(req, ctx).await?;
537 let body: HasPermissionRequest = match better_auth_core::validate_request_body(req) {
538 Ok(v) => v,
539 Err(resp) => return Ok(resp),
540 };
541 let response = has_permission_core(&body, &user, &self.config)?;
542 AuthResponse::json(200, &response).map_err(AuthError::from)
543 }
544}
545
546pub(super) fn target_is_admin(
547 user_id: Option<&str>,
548 role: Option<&str>,
549 config: &AdminConfig,
550) -> bool {
551 is_admin_user_id(user_id, config) || is_admin_role(role, config)
552}
553
554pub use access::RolePermissions;