1use std::collections::HashMap;
2
3use better_auth_core::entity::{AuthSession, AuthUser};
4use better_auth_core::utils::cookie_utils::{
5 create_clear_cookie, create_session_like_cookie, related_cookie_name,
6};
7use better_auth_core::wire::{SessionView, UserView};
8use better_auth_core::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult};
9
10pub mod access;
11mod banned_message;
12mod native;
13mod permission_api;
14mod request;
15pub use banned_message::{BannedUserMessage, BannedUserMessageFuture};
16pub use native::AdminApi;
17pub use permission_api::{AdminPermissionApi, AdminPermissionError, AuthorizedSession};
18pub use types::{
19 CreateUserRequest as CreateAdminUser, RoleInput as AdminRole, UserResponse as AdminUserResponse,
20};
21pub(super) mod handlers;
22pub(super) mod types;
23
24#[cfg(test)]
25mod tests;
26
27use crate::plugins::helpers::{delete_session_cookies, get_cookie};
28use access::{has_permission, is_admin_role, is_admin_user_id};
29use handlers::*;
30use types::*;
31
32const MESSAGE_CHANGE_ROLE: &str = "You are not allowed to change users role";
33const MESSAGE_CREATE_USERS: &str = "You are not allowed to create users";
34const MESSAGE_LIST_USERS: &str = "You are not allowed to list users";
35const MESSAGE_LIST_USER_SESSIONS: &str = "You are not allowed to list users sessions";
36const MESSAGE_BAN_USERS: &str = "You are not allowed to ban users";
37const MESSAGE_IMPERSONATE_USERS: &str = "You are not allowed to impersonate users";
38const MESSAGE_REVOKE_USER_SESSIONS: &str = "You are not allowed to revoke users sessions";
39const MESSAGE_DELETE_USERS: &str = "You are not allowed to delete users";
40const MESSAGE_SET_USER_PASSWORD: &str = "You are not allowed to set users password";
41const MESSAGE_GET_USER: &str = "You are not allowed to get user";
42const MESSAGE_UPDATE_USERS: &str = "You are not allowed to update users";
43
44pub struct AdminPlugin {
46 config: AdminConfig,
47}
48
49#[derive(Debug, Clone, better_auth_core::PluginConfig)]
51#[plugin(name = "AdminPlugin")]
52pub struct AdminConfig {
53 #[config(default = "user".to_string())]
55 pub default_role: String,
56 #[config(default = None)]
58 pub admin_roles: Option<Vec<String>>,
59 #[config(default = None)]
61 pub admin_user_ids: Option<Vec<String>>,
62 #[config(default = None)]
65 pub roles: Option<HashMap<String, access::RolePermissions>>,
66 #[config(default = None)]
68 pub default_ban_reason: Option<String>,
69 #[config(default = None)]
71 pub default_ban_expires_in: Option<f64>,
72 #[config(default = None)]
74 pub impersonation_session_duration: Option<f64>,
75 #[config(default = BannedUserMessage::default(), skip)]
77 pub banned_user_message: BannedUserMessage,
78 #[config(default = false)]
80 pub allow_impersonating_admins: bool,
81}
82
83better_auth_core::impl_auth_plugin! {
84 AdminPlugin, "admin";
85 routes {
86 post "/admin/set-role" => handle_set_role, "setUserRole", body = request::validate, require_headers = true;
87 get "/admin/get-user" => handle_get_user, "getUser", query = crate::plugins::query_input::get_user;
88 post "/admin/create-user" => handle_create_user, "createUser", body = request::validate;
89 post "/admin/update-user" => handle_update_user, "adminUpdateUser", body = request::validate;
90 get "/admin/list-users" => handle_list_users, "listUsers", query = crate::plugins::query_input::list_users;
91 post "/admin/list-user-sessions" => handle_list_user_sessions, "adminListUserSessions", body = request::validate;
92 post "/admin/ban-user" => handle_ban_user, "banUser", body = request::validate;
93 post "/admin/unban-user" => handle_unban_user, "unbanUser", body = request::validate;
94 post "/admin/impersonate-user" => handle_impersonate_user, "impersonateUser", body = request::validate;
95 post "/admin/stop-impersonating" => handle_stop_impersonating, "stopImpersonating", require_headers = true;
96 post "/admin/revoke-user-session" => handle_revoke_user_session, "revokeUserSession", body = request::validate;
97 post "/admin/revoke-user-sessions" => handle_revoke_user_sessions, "revokeUserSessions", body = request::validate;
98 post "/admin/remove-user" => handle_remove_user, "removeUser", body = request::validate;
99 post "/admin/set-user-password" => handle_set_user_password, "setUserPassword", body = request::validate;
100 post "/admin/has-permission" => handle_has_permission, "userHasPermission", body = request::validate;
101 }
102 extra {
103 async fn on_init(
104 &self,
105 ctx: &mut better_auth_core::AuthInitContext<S>,
106 ) -> better_auth_core::AuthResult<()> {
107 self.config.validate()?;
108 S::User::require_plugin_fields("admin", &["role", "banned", "ban_reason", "ban_expires"])?;
109 S::Session::require_plugin_fields("admin", &["impersonated_by"])?;
110 ctx.set_metadata("admin.enabled", serde_json::Value::Bool(true));
111 ctx.set_metadata(
112 "admin.default_role",
113 serde_json::Value::String(self.config.default_role.clone()),
114 );
115 ctx.extensions.insert(self.config.banned_user_message.clone());
116 ctx.extensions.insert(self.config.clone());
117 Ok(())
118 }
119 }
120}
121
122impl AdminPlugin {
123 pub fn banned_user_message(mut self, message: impl Into<BannedUserMessage>) -> Self {
125 self.config.banned_user_message = message.into();
126 self
127 }
128
129 async fn require_session(
130 &self,
131 req: &AuthRequest,
132 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
133 ) -> AuthResult<(UserView, SessionView)> {
134 ctx.require_authoritative_session(req)
135 .await
136 .map_err(|error| match error {
137 AuthError::Unauthenticated => AuthResponse::new(401).into(),
138 error => error,
139 })
140 }
141
142 async fn optional_session(
143 &self,
144 req: &AuthRequest,
145 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
146 ) -> AuthResult<Option<(UserView, SessionView)>> {
147 let data = ctx
148 .session_manager()
149 .resolve(req, better_auth_core::session::SessionRead::Authoritative)
150 .await?
151 .data;
152 if data.is_none() && (req.endpoint_headers().is_some() || req.original_request().is_some())
153 {
154 return Err(AuthResponse::new(401).into());
155 }
156 Ok(data.map(|data| (data.user, data.session)))
157 }
158
159 fn authorize(
160 &self,
161 user: &UserView,
162 resource: &str,
163 action: &str,
164 message: &str,
165 ) -> AuthResult<()> {
166 let permissions = HashMap::from([(resource.to_string(), vec![action.to_string()])]);
167 if has_permission(
168 user.id.as_str(),
169 user.role.as_deref(),
170 &self.config,
171 &permissions,
172 ) {
173 Ok(())
174 } else {
175 Err(AuthError::forbidden(message))
176 }
177 }
178
179 async fn handle_set_role(
180 &self,
181 req: &AuthRequest,
182 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
183 ) -> AuthResult<AuthResponse> {
184 let (user, _session) = self.require_session(req, ctx).await?;
185 self.authorize(&user, "user", "set-role", MESSAGE_CHANGE_ROLE)?;
186 let body: SetRoleRequest = request::read(req)?;
187 let response = set_role_core(&body, &self.config, ctx).await?;
188 AuthResponse::json(200, &response).map_err(AuthError::from)
189 }
190
191 async fn handle_get_user(
192 &self,
193 req: &AuthRequest,
194 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
195 ) -> AuthResult<AuthResponse> {
196 let (user, _session) = self.require_session(req, ctx).await?;
197 self.authorize(&user, "user", "get", MESSAGE_GET_USER)?;
198 let query = GetUserQuery {
199 id: req
200 .query_string("id")?
201 .map(str::to_owned)
202 .unwrap_or_default(),
203 };
204 let response = get_user_core(&query, ctx).await?;
205 AuthResponse::json(200, &response).map_err(AuthError::from)
206 }
207
208 async fn handle_create_user(
209 &self,
210 req: &AuthRequest,
211 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
212 ) -> AuthResult<AuthResponse> {
213 let body: CreateUserRequest = request::read(req)?;
214 let session = self.optional_session(req, ctx).await?;
215 if let Some((user, _)) = &session {
216 self.authorize(user, "user", "create", MESSAGE_CREATE_USERS)?;
217 }
218 let response = create_user_core(&body, Some(req), session, &self.config, ctx).await?;
219 AuthResponse::json(200, &response).map_err(AuthError::from)
220 }
221
222 async fn handle_update_user(
223 &self,
224 req: &AuthRequest,
225 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
226 ) -> AuthResult<AuthResponse> {
227 let (user, _session) = self.require_session(req, ctx).await?;
228 self.authorize(&user, "user", "update", MESSAGE_UPDATE_USERS)?;
229 let body: AdminUpdateUserRequest = request::read(req)?;
230 let response = update_user_core(&body, &user, &self.config, ctx).await?;
231 AuthResponse::json(200, &response).map_err(AuthError::from)
232 }
233
234 async fn handle_list_users(
235 &self,
236 req: &AuthRequest,
237 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
238 ) -> AuthResult<AuthResponse> {
239 let (user, _session) = self.require_session(req, ctx).await?;
240 self.authorize(&user, "user", "list", MESSAGE_LIST_USERS)?;
241 let query: ListUsersQueryParams = crate::plugins::query_input::parse(&req.query)?;
242 let response = list_users_core(&query, ctx)
244 .await
245 .unwrap_or_else(|_| ListUsersResponse {
246 users: Vec::new(),
247 total: 0,
248 limit: None,
249 offset: None,
250 });
251 AuthResponse::json(200, &response).map_err(AuthError::from)
252 }
253
254 async fn handle_list_user_sessions(
255 &self,
256 req: &AuthRequest,
257 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
258 ) -> AuthResult<AuthResponse> {
259 let (user, _session) = self.require_session(req, ctx).await?;
260 self.authorize(&user, "session", "list", MESSAGE_LIST_USER_SESSIONS)?;
261 let body: UserIdRequest = request::read(req)?;
262 let response = list_user_sessions_core(&body, ctx).await?;
263 AuthResponse::json(200, &response).map_err(AuthError::from)
264 }
265
266 async fn handle_ban_user(
267 &self,
268 req: &AuthRequest,
269 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
270 ) -> AuthResult<AuthResponse> {
271 let (user, _session) = self.require_session(req, ctx).await?;
272 self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
273 let body: BanUserRequest = request::read(req)?;
274 let response = ban_user_core(&body, user.id.typed()?.as_str(), &self.config, ctx).await?;
275 AuthResponse::json(200, &response).map_err(AuthError::from)
276 }
277
278 async fn handle_unban_user(
279 &self,
280 req: &AuthRequest,
281 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
282 ) -> AuthResult<AuthResponse> {
283 let (user, _session) = self.require_session(req, ctx).await?;
284 self.authorize(&user, "user", "ban", MESSAGE_BAN_USERS)?;
285 let body: UserIdRequest = request::read(req)?;
286 let response = unban_user_core(&body, ctx).await?;
287 AuthResponse::json(200, &response).map_err(AuthError::from)
288 }
289
290 async fn handle_impersonate_user(
291 &self,
292 req: &AuthRequest,
293 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
294 ) -> AuthResult<AuthResponse> {
295 let (user, session) = self.require_session(req, ctx).await?;
296 self.authorize(&user, "user", "impersonate", MESSAGE_IMPERSONATE_USERS)?;
297 let body: UserIdRequest = request::read(req)?;
298 let (response, data) = impersonate_user_core(
299 &body,
300 &user,
301 ctx.config.advanced.ip_address.resolve(req).as_deref(),
302 req.headers.get("user-agent").map(|value| value.as_str()),
303 &self.config,
304 ctx,
305 )
306 .await?;
307 let dont_remember = ctx.session_manager().dont_remember(req);
308 let admin_cookie = create_admin_session_cookie_value(
309 ctx.config.signing_secret(),
310 &AdminSessionCookiePayload {
311 session_token: session.token.clone(),
312 dont_remember,
313 },
314 ctx.config.session.expires_in(),
315 )?;
316 let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
317
318 delete_session_cookies(req, &ctx.config, false, None)?;
319 req.append_response_header(
320 "Set-Cookie",
321 create_session_like_cookie(
322 &admin_cookie_name,
323 &admin_cookie,
324 Some(ctx.config.session.expires_in().as_seconds_f64()),
325 &ctx.config,
326 )?,
327 )?;
328 ctx.session_manager()
329 .set_session_cookie(req, data, Some(true))
330 .await?;
331 let auth_response = AuthResponse::json(200, &response)?;
332 Ok(auth_response)
333 }
334
335 async fn handle_stop_impersonating(
336 &self,
337 req: &AuthRequest,
338 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
339 ) -> AuthResult<AuthResponse> {
340 let session_manager = ctx.session_manager();
341 let session = session_manager
342 .resolve(req, better_auth_core::session::SessionRead::Authoritative)
343 .await?
344 .data
345 .ok_or_else(|| AuthError::from(AuthResponse::new(401)))?
346 .session;
347 if session.impersonated_by.is_none() {
348 return Err(AuthError::bad_request("You are not impersonating anyone"));
349 }
350
351 let admin_cookie_name = related_cookie_name(&ctx.config, "admin_session");
352 let admin_cookie_value = get_cookie(req, &admin_cookie_name)
353 .ok_or_else(|| AuthError::internal("Failed to find admin session"))?;
354 let admin_cookie =
355 decode_admin_session_cookie_value(ctx.config.signing_secret(), &admin_cookie_value)
356 .map_err(|_| AuthError::internal("Failed to find admin session"))?;
357
358 let (response, data) = stop_impersonating_core(&session, &admin_cookie, ctx).await?;
359
360 ctx.session_manager()
361 .set_session_cookie(req, data, Some(admin_cookie.dont_remember))
362 .await?;
363 let mut auth_response = AuthResponse::json(200, &response)?;
364 better_auth_core::utils::cookie_utils::remove_set_cookie_entries(
365 req,
366 Some(&mut auth_response.headers),
367 &admin_cookie_name,
368 )?;
369 auth_response = auth_response.with_appended_header(
370 "Set-Cookie",
371 create_clear_cookie(&admin_cookie_name, &ctx.config)?,
372 );
373 Ok(auth_response)
374 }
375
376 async fn handle_revoke_user_session(
377 &self,
378 req: &AuthRequest,
379 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
380 ) -> AuthResult<AuthResponse> {
381 let (user, _session) = self.require_session(req, ctx).await?;
382 self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
383 let body: RevokeSessionRequest = request::read(req)?;
384 let response = revoke_user_session_core(&body, ctx).await?;
385 AuthResponse::json(200, &response).map_err(AuthError::from)
386 }
387
388 async fn handle_revoke_user_sessions(
389 &self,
390 req: &AuthRequest,
391 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
392 ) -> AuthResult<AuthResponse> {
393 let (user, _session) = self.require_session(req, ctx).await?;
394 self.authorize(&user, "session", "revoke", MESSAGE_REVOKE_USER_SESSIONS)?;
395 let body: UserIdRequest = request::read(req)?;
396 let response = revoke_user_sessions_core(&body, ctx).await?;
397 AuthResponse::json(200, &response).map_err(AuthError::from)
398 }
399
400 async fn handle_remove_user(
401 &self,
402 req: &AuthRequest,
403 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
404 ) -> AuthResult<AuthResponse> {
405 let (user, _session) = self.require_session(req, ctx).await?;
406 self.authorize(&user, "user", "delete", MESSAGE_DELETE_USERS)?;
407 let body: UserIdRequest = request::read(req)?;
408 let response = remove_user_core(&body, user.id.typed()?.as_str(), ctx).await?;
409 AuthResponse::json(200, &response).map_err(AuthError::from)
410 }
411
412 async fn handle_set_user_password(
413 &self,
414 req: &AuthRequest,
415 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
416 ) -> AuthResult<AuthResponse> {
417 let (user, _session) = self.require_session(req, ctx).await?;
418 self.authorize(&user, "user", "set-password", MESSAGE_SET_USER_PASSWORD)?;
419 let body: SetUserPasswordRequest = request::read(req)?;
420 let response = set_user_password_core(&body, ctx).await?;
421 AuthResponse::json(200, &response).map_err(AuthError::from)
422 }
423
424 async fn handle_has_permission(
425 &self,
426 req: &AuthRequest,
427 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
428 ) -> AuthResult<AuthResponse> {
429 let body: HasPermissionRequest = request::read(req)?;
430 let requested = body.requested_permissions().ok_or_else(|| {
431 AuthError::bad_request("invalid permission check. no permission(s) were passed.")
432 })?;
433 let session = self.optional_session(req, ctx).await?;
434 let user_id = body.user_id.as_deref().filter(|value| !value.is_empty());
435 let role = body.role.as_deref().filter(|value| !value.is_empty());
436 if session.is_none() && user_id.is_none() && role.is_none() {
437 return Err(AuthError::bad_request("user id or role is required"));
438 }
439 let user = match session {
440 Some((user, _)) => Some(user),
441 None if role.is_some() => None,
442 None => Some(
443 ctx.database
444 .get_user_by_id(
445 user_id.ok_or_else(|| AuthError::bad_request("user not found"))?,
446 )
447 .await?
448 .ok_or_else(|| AuthError::bad_request("user not found"))?,
449 ),
450 };
451 let response = match user {
452 Some(user) => has_permission_core(&body, &user, &self.config)?,
453 None => PermissionResponse {
454 error: None,
455 success: has_permission(user_id, role, &self.config, requested),
456 },
457 };
458 AuthResponse::json(200, &response).map_err(AuthError::from)
459 }
460}
461
462pub(super) fn target_is_admin(
463 user_id: Option<&str>,
464 role: Option<&str>,
465 config: &AdminConfig,
466) -> bool {
467 is_admin_user_id(user_id, config) || is_admin_role(role, config)
468}
469
470pub use access::RolePermissions;