rustauth_plugins/admin/
mod.rs1mod access;
4mod cookies;
5mod errors;
6mod handlers;
7mod models;
8mod openapi;
9mod options;
10mod response;
11mod routes;
12mod schema;
13mod store;
14
15pub use access::{has_permission, AdminRole, PermissionMap};
16pub use errors::ADMIN_ERROR_CODES;
17pub use models::{AdminSession, AdminUser};
18pub use options::{AdminOptions, AdminOptionsBuilder, AdminSchemaOptions};
19
20use rustauth_core::error::RustAuthError;
21use rustauth_core::plugin::{
22 AuthPlugin, PluginAfterHookAction, PluginDatabaseBeforeAction, PluginDatabaseBeforeInput,
23 PluginDatabaseHook,
24};
25use serde_json::Value;
26use time::OffsetDateTime;
27
28pub mod access_control {
29 pub use super::access::{
30 default_access_control, default_roles, default_statements, has_permission, AdminRole,
31 PermissionMap,
32 };
33}
34
35pub const UPSTREAM_PLUGIN_ID: &str = "admin";
36
37pub fn admin(options: AdminOptions) -> Result<AuthPlugin, RustAuthError> {
38 let options = options.with_defaults();
39 options.validate().map_err(RustAuthError::InvalidConfig)?;
40 Ok(build_admin_plugin(options))
41}
42
43fn build_admin_plugin(options: AdminOptions) -> AuthPlugin {
44 AuthPlugin::new(UPSTREAM_PLUGIN_ID)
45 .with_version(crate::VERSION)
46 .with_options(options.to_json())
47 .with_schema(schema::user_role_field(&options.schema))
48 .with_schema(schema::user_banned_field(&options.schema))
49 .with_schema(schema::user_ban_reason_field(&options.schema))
50 .with_schema(schema::user_ban_expires_field(&options.schema))
51 .with_schema(schema::session_impersonated_by_field(&options.schema))
52 .with_database_hook(default_role_hook(options.default_role.clone()))
53 .with_database_hook(banned_session_hook(options.banned_user_message.clone()))
54 .with_async_after_hook("/list-sessions", filter_impersonated_sessions_hook)
55 .with_endpoint(routes::set_role(options.clone()))
56 .with_endpoint(routes::get_user(options.clone()))
57 .with_endpoint(routes::create_user(options.clone()))
58 .with_endpoint(routes::update_user(options.clone()))
59 .with_endpoint(routes::list_users(options.clone()))
60 .with_endpoint(routes::list_user_sessions(options.clone()))
61 .with_endpoint(routes::ban_user(options.clone()))
62 .with_endpoint(routes::unban_user(options.clone()))
63 .with_endpoint(routes::impersonate_user(options.clone()))
64 .with_endpoint(routes::stop_impersonating())
65 .with_endpoint(routes::revoke_user_session(options.clone()))
66 .with_endpoint(routes::revoke_user_sessions(options.clone()))
67 .with_endpoint(routes::remove_user(options.clone()))
68 .with_endpoint(routes::set_user_password(options.clone()))
69 .with_endpoint(routes::has_permission_endpoint(options.clone()))
70 .with_error_code(errors::failed_to_create_user())
71 .with_error_code(errors::user_already_exists())
72 .with_error_code(errors::user_already_exists_use_another_email())
73 .with_error_code(errors::cannot_ban_yourself())
74 .with_error_code(errors::not_allowed_to_change_role())
75 .with_error_code(errors::not_allowed_to_create_users())
76 .with_error_code(errors::not_allowed_to_list_users())
77 .with_error_code(errors::not_allowed_to_list_sessions())
78 .with_error_code(errors::not_allowed_to_ban_users())
79 .with_error_code(errors::not_allowed_to_impersonate_users())
80 .with_error_code(errors::not_allowed_to_revoke_sessions())
81 .with_error_code(errors::not_allowed_to_delete_users())
82 .with_error_code(errors::not_allowed_to_set_password())
83 .with_error_code(errors::banned_user(&options.banned_user_message))
84 .with_error_code(errors::not_allowed_to_get_user())
85 .with_error_code(errors::no_data_to_update())
86 .with_error_code(errors::not_allowed_to_update_users())
87 .with_error_code(errors::cannot_remove_yourself())
88 .with_error_code(errors::not_allowed_to_set_unknown_role())
89 .with_error_code(errors::cannot_impersonate_admins())
90 .with_error_code(errors::invalid_role_type())
91}
92
93fn default_role_hook(default_role: String) -> PluginDatabaseHook {
94 PluginDatabaseHook::before_create("admin_default_user_role", move |_context, mut query| {
95 if query.model == "user" && !query.data.contains_key("role") {
96 query.data.insert(
97 "role".to_owned(),
98 rustauth_core::db::DbValue::String(default_role.clone()),
99 );
100 }
101 Ok(PluginDatabaseBeforeAction::Continue(
102 PluginDatabaseBeforeInput::Create(query),
103 ))
104 })
105}
106
107fn banned_session_hook(message: String) -> PluginDatabaseHook {
108 PluginDatabaseHook::before_create_async(
109 "admin_block_banned_user_session",
110 move |context, query| {
111 let message = message.clone();
112 Box::pin(async move {
113 if query.model != "session" {
114 return Ok(PluginDatabaseBeforeAction::Continue(
115 PluginDatabaseBeforeInput::Create(query),
116 ));
117 }
118 let Some(rustauth_core::db::DbValue::String(user_id)) = query.data.get("user_id")
119 else {
120 return Ok(PluginDatabaseBeforeAction::Continue(
121 PluginDatabaseBeforeInput::Create(query),
122 ));
123 };
124 let store = store::AdminStore::new(context.adapter);
125 let Some(user) = store.find_user_by_id(user_id).await? else {
126 return Ok(PluginDatabaseBeforeAction::Continue(
127 PluginDatabaseBeforeInput::Create(query),
128 ));
129 };
130 if !user.banned {
131 return Ok(PluginDatabaseBeforeAction::Continue(
132 PluginDatabaseBeforeInput::Create(query),
133 ));
134 }
135 if user
136 .ban_expires
137 .is_some_and(|expires| expires < OffsetDateTime::now_utc())
138 {
139 store.unban_user(user_id).await?;
140 return Ok(PluginDatabaseBeforeAction::Continue(
141 PluginDatabaseBeforeInput::Create(query),
142 ));
143 }
144 if context.request_path.as_deref().is_some_and(|path| {
145 path.starts_with("/callback") || path.starts_with("/oauth2/callback")
146 }) {
147 return Ok(PluginDatabaseBeforeAction::Cancel(RustAuthError::Api(
148 format!("BANNED_USER: {message}"),
149 )));
150 }
151 Ok(PluginDatabaseBeforeAction::Cancel(RustAuthError::Api(
152 format!("BANNED_USER: {message}"),
153 )))
154 })
155 },
156 )
157}
158
159fn filter_impersonated_sessions_hook<'a>(
160 context: &'a rustauth_core::context::AuthContext,
161 _request: &'a rustauth_core::api::ApiRequest,
162 response: rustauth_core::api::ApiResponse,
163) -> rustauth_core::plugin::PluginAfterHookFuture<'a> {
164 Box::pin(async move {
165 let Some(adapter) = context.adapter() else {
166 return Ok(PluginAfterHookAction::Continue(response));
167 };
168 let (parts, body) = response.into_parts();
169 let Ok(Value::Array(sessions)) = serde_json::from_slice::<Value>(&body) else {
170 return Ok(PluginAfterHookAction::Continue(http::Response::from_parts(
171 parts, body,
172 )));
173 };
174 let store = store::AdminStore::new(adapter.as_ref());
175 let mut filtered = Vec::new();
176 for session in sessions {
177 let Some(token) = session.get("token").and_then(Value::as_str) else {
178 filtered.push(session);
179 continue;
180 };
181 match store.find_session(token).await? {
182 Some((admin_session, _)) if admin_session.impersonated_by.is_none() => {
183 filtered.push(session);
184 }
185 None => filtered.push(session),
186 Some(_) => {}
187 }
188 }
189 let body = serde_json::to_vec(&filtered).map_err(|error| {
190 RustAuthError::Api(format!("failed to serialize filtered sessions: {error}"))
191 })?;
192 Ok(PluginAfterHookAction::Continue(http::Response::from_parts(
193 parts, body,
194 )))
195 })
196}